From cc1dc3f1c6f88cd03665f933e0b7590e5717b0fe Mon Sep 17 00:00:00 2001 From: Prophet069 <291514246+Prophet069@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:48:09 +0200 Subject: [PATCH 001/318] fix(shader): decode GFX10 scratch offsets as one dword --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 12 +++++ .../prx/libSceAgcDriver/tests/FlatScratch.cpp | 48 +++++++++++++++++++ .../RdnaDecoder/src/RdnaMemoryOpDecoder.cpp | 17 +++++++ .../MemoryResourceHelpers.cpp | 2 +- docs/dev/debt/shader.md | 4 ++ 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8b5909b8e0..311edda0ec 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1966,3 +1966,15 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") 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:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp b/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp new file mode 100644 index 0000000000..4db6cb7c5b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp @@ -0,0 +1,48 @@ +#include "Translation/TranslationContext.hpp" +#include "RdnaDecoder/RdnaMemoryOpDecoder.hpp" +#include +#include +#include +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 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 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 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); +} diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp index 77ea8f0322..886e14be32 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -932,6 +932,23 @@ RdnaInstruction DecodeRdnaFlat(std::uint32_t programCounter, std::span Date: Mon, 19 Oct 2026 07:09:47 -0500 Subject: [PATCH 002/318] fix(shader): resolve table column pointers through Phi nodes --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 42 +++++++++++++++++ .../Optimization/src/ResourceTracker.cpp | 46 ++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index e788638438..058046a306 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3042,6 +3042,18 @@ void verifyPointerImageTable() { require(!TableTouched(narrowTable.regions, root + (Palette + 32u) / 4u, 32u) && !TableTouched(narrowTable.regions, root + Committed / 4u, Reserved - Committed), "pointer image table: a record outside the key domain was read"); const std::vector> narrowReads{{base + Palette, 32u}, {base + Palette + 2u * 32u, 32u}, {base + Palette + 255u * 32u, 32u}, {base + Palette + 0x102u * 32u, 32u}}; require(narrowSnapshot.readRanges == narrowReads && narrowTable.result->imageTableReadRanges.size() == 1u && narrowTable.result->imageTableReadRanges[0] == narrowReads, "pointer image table: consumed ranges include skipped keys or the key-domain buffer"); + + auto phiCode = maskedCode; + phiCode.erase(phiCode.begin() + 8, phiCode.begin() + 10); + phiCode.insert(phiCode.begin() + 8, { + 0xbf068010u, 0xbf850003u, 0xf4200444u, 0x20000000u, 0xbf820002u, 0xf4200444u, 0x20000000u + }); + const auto phiRequest = request(phiCode, 0x36000u); + const auto phiPlan = GetResourcePlan(phiRequest); + require(phiPlan->info.images.size() == 1u && phiPlan->info.images[0].table == 0u, "pointer image table: a pointer read through a phi was not planned as a table"); + const auto phiTable = CompileTable(phiRequest); + require(phiTable.capture->snapshot.tables.tables.at(0).codes == snapshot.codes, "pointer image table: a pointer read through a phi captured different records"); + keys = {}; #ifdef _WIN32 VirtualFree(block, 0, MEM_RELEASE); @@ -3445,6 +3457,36 @@ void verifyPointerChainImageTable() { static_cast(ValidateAndOptimizeSpirv(compiled.result->spirv, request.target.vulkanVersion, request.target.spirvVersion)); #endif store(links[1].data() + 4u, address(descriptors[1].data())); + + auto phiCode = code; + phiCode.erase(phiCode.begin() + 15, phiCode.begin() + 17); + phiCode.insert(phiCode.begin() + 15, { + 0xbf068010u, 0xbf850003u, 0xf4240b02u, 0x24000008u, 0xbf820002u, 0xf4240b02u, 0x24000008u + }); + auto phiRequest = TableRequest(phiCode, userData); + phiRequest.shader.codeAddress = 0x38000u; + const auto phiPlan = GetResourcePlan(phiRequest); + require(phiPlan->info.images.size() == 1u && phiPlan->info.images[0].table == 0u, "pointer chain image table: a pointer read through a phi was not planned as a table"); + const auto phiTable = CompileTable(phiRequest); + require(phiTable.capture->snapshot.tables.tables.at(0).codes == snapshot.codes, "pointer chain image table: a pointer read through a phi captured different records"); + + auto divergentCode = code; + divergentCode.erase(divergentCode.begin() + 15, divergentCode.begin() + 17); + divergentCode.insert(divergentCode.begin() + 15, { + 0xbf068010u, 0xbf850003u, 0xf4240b02u, 0x24000008u, 0xbf820002u, 0xf4240b02u, 0x24000018u + }); + auto divergentRequest = TableRequest(divergentCode, userData); + divergentRequest.shader.codeAddress = 0x39000u; + expectFailure([&] { static_cast(GetResourcePlan(divergentRequest)); }, "GetImageResource dword 0 is not a valid runtime value", "pointer chain image table: a phi reading different table offsets was planned as a table"); + + auto nonTableCode = code; + nonTableCode.erase(nonTableCode.begin() + 15, nonTableCode.begin() + 17); + nonTableCode.insert(nonTableCode.begin() + 15, { + 0xbf068010u, 0xbf850003u, 0xf4240b02u, 0x24000008u, 0xbf820002u, 0xbe960380u, 0xbe970380u + }); + auto nonTableRequest = TableRequest(nonTableCode, userData); + nonTableRequest.shader.codeAddress = 0x3a000u; + expectFailure([&] { static_cast(GetResourcePlan(nonTableRequest)); }, "GetImageResource dword 0 is not a valid runtime value", "pointer chain image table: a phi with a non-table branch was planned as a table"); } void verifyDescriptorPhis() { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 7306a87c0e..ceb8017633 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -691,6 +691,47 @@ private: return true; } + IrValue* UnwrapTableRead(IrValue* value) const { + if (value == nullptr) return nullptr; + value = value->Resolve(); + if (!value->IsPhi()) return value; + IrValue* candidate = nullptr; + std::vector pending{value}; + std::unordered_set visited; + while (!pending.empty()) { + auto* current = pending.back()->Resolve(); + pending.pop_back(); + if (!visited.insert(current).second) continue; + if (current->IsPhi()) { + for (std::size_t i = 0; i < current->ArgumentCount(); i++) { + pending.push_back(current->Argument(i)); + } + continue; + } + std::uint32_t memoryIndex = 0; + IrValue* key = nullptr; + std::uint32_t stride = 0; + std::uint32_t addend = 0; + if (ScalarReadMemory(*current, memoryIndex) == nullptr || !MemoryIndexBelongsTo(memoryIndex, *current) || + !MatchAffineOffset(current->Argument(1), key, stride, addend)) { + return nullptr; + } + if (candidate == nullptr) { + candidate = current; + } else if (!EquivalentValue(m_program.Resources(), candidate, current)) { + std::uint32_t candidateIndex = 0; + const auto* candMem = ScalarReadMemory(*candidate, candidateIndex); + const auto* curMem = ScalarReadMemory(*current, memoryIndex); + if (candMem == nullptr || curMem == nullptr || candMem->offset != curMem->offset || + !EquivalentValue(m_program.Resources(), candidate->Argument(0), current->Argument(0)) || + !EquivalentValue(m_program.Resources(), candidate->Argument(1), current->Argument(1))) { + return nullptr; + } + } + } + return candidate; + } + bool TryMakeIndirectColumn(IrValue& handle, IrValue& descriptorPointer, bool sampler, std::uint32_t words, std::uint32_t descriptorOffset, std::uint32_t descriptorImmediate, TableColumnPlan& plan) { std::vector hops; IrValue* pointer = &descriptorPointer; @@ -728,7 +769,10 @@ private: IrValue* offset = nullptr; std::uint32_t first = 0; for (std::uint32_t dword = 0; dword < 2u; dword++) { - IrValue* read = pointer->Argument(dword)->Resolve(); + IrValue* read = UnwrapTableRead(pointer->Argument(dword)); + if (read == nullptr) { + return false; + } std::uint32_t memoryIndex = 0; const MemoryInfo* memory = ScalarReadMemory(*read, memoryIndex); if (memory == nullptr || !MemoryIndexBelongsTo(memoryIndex, *read)) { From 021175cca7e4452214e97991039dd0aae8d49b4d Mon Sep 17 00:00:00 2001 From: klosixgaming-arch Date: Mon, 19 Oct 2026 07:20:07 -0500 Subject: [PATCH 003/318] fix(shader): deduplicate sampler table elements in ResolveImageTables Deduplicate sampler descriptor records across image tables during table resolution so identical sampler descriptors share element table indices rather than exceeding the hardware sampler element budget. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 80 +++++++++++++++++++ .../Optimization/src/ResourceMaterializer.cpp | 34 +++++--- 2 files changed, 103 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index e788638438..0cd4cc9ec7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -7140,6 +7140,85 @@ void verifyVolumeBiasTableRejection() { } } +void verifySamplerTableDeduplication() { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + SamplerResource sampler0{}; + sampler0.table = 0u; + SamplerResource sampler1{}; + sampler1.table = 1u; + ShaderInfo info{}; + info.samplers.push_back(sampler0); + info.samplers.push_back(sampler1); + ResourceSnapshot snapshot{}; + snapshot.tables.tables.resize(2u); + const DescriptorValue s0{{0x00000600u, 0x00fff000u, 0x06f03fa0u, 0u}, 4u}; + const DescriptorValue s1{{0x00000700u, 0x00fff000u, 0x06f03fa0u, 0u}, 4u}; + const DescriptorValue s2{{0x00000800u, 0x00fff000u, 0x06f03fa0u, 0u}, 4u}; + std::vector codes(300u); + for (std::size_t i = 0; i < 300u; ++i) codes[i] = static_cast(i % 3u); + for (std::uint32_t table = 0u; table < 2u; ++table) { + auto& column = snapshot.tables.tables[table]; + column.records = 300u; + column.keys = 300u; + column.codes = SharedValues(codes); + column.words = SharedValues(std::vector{s0, s1, s2}); + } + const auto resolved = ResourceMaterializer::ResolveImageTables(info, snapshot); + require(resolved.samplers.size() == (3u + 1u) * Abi::SamplerEntryElements, "sampler table deduplication: duplicate sampler entries across tables were not deduplicated"); + for (std::uint32_t table = 0u; table < 2u; ++table) { + const auto mapStart = resolved.map.at(Abi::TableHeader(table) + Abi::TableMapStart); + for (std::uint32_t key = 0; key < 300u; ++key) { + const auto record = resolved.map.at(mapStart + key); + const auto element = resolved.map.at(record + Abi::SamplerRecordElement); + require(element == (key % 3u) * Abi::SamplerEntryElements, "sampler table deduplication: record points to the wrong deduplicated sampler element"); + require((resolved.map.at(record + Abi::SamplerRecordFlags) & Abi::PoisonFlag) == 0u, "sampler table deduplication: deduplicated sampler was poisoned"); + } + } + + constexpr std::uint32_t MaxUnique = Abi::SamplerElementBudget / Abi::SamplerEntryElements; + std::vector budgetWords; + budgetWords.reserve(MaxUnique + 1u); + for (std::uint32_t i = 0; i < MaxUnique + 1u; ++i) { + budgetWords.push_back(DescriptorValue{{0x00000600u, 0x00fff000u, 0x06f03fa0u, i + 1u}, 4u}); + } + std::vector budgetCodes(MaxUnique + 2u); + for (std::uint32_t i = 0; i < MaxUnique; ++i) budgetCodes[i] = i; + budgetCodes[MaxUnique] = 0u; + budgetCodes[MaxUnique + 1u] = MaxUnique; + + ShaderInfo budgetInfo{}; + SamplerResource budgetSampler{}; + budgetSampler.table = 0u; + budgetInfo.samplers.push_back(budgetSampler); + ResourceSnapshot budgetSnapshot{}; + budgetSnapshot.tables.tables.resize(1u); + auto& budgetColumn = budgetSnapshot.tables.tables[0]; + budgetColumn.records = static_cast(budgetCodes.size()); + budgetColumn.keys = static_cast(budgetCodes.size()); + budgetColumn.codes = SharedValues(budgetCodes); + budgetColumn.words = SharedValues(budgetWords); + + const auto budgetResolved = ResourceMaterializer::ResolveImageTables(budgetInfo, budgetSnapshot); + require(budgetResolved.samplers.size() == Abi::SamplerElementBudget, "sampler table budget: valid unique samplers did not fill budget"); + const auto budgetMapStart = budgetResolved.map.at(Abi::TableHeader(0) + Abi::TableMapStart); + + const auto dupRecord = budgetResolved.map.at(budgetMapStart + MaxUnique); + const auto dupFlags = budgetResolved.map.at(dupRecord + Abi::SamplerRecordFlags); + require((dupFlags & Abi::PoisonFlag) == 0u, "sampler table budget: duplicate of existing sampler was poisoned when budget is full"); + require(budgetResolved.map.at(dupRecord + Abi::SamplerRecordElement) == 0u, "sampler table budget: duplicate sampler did not reuse existing element"); + + const auto overflowRecord = budgetResolved.map.at(budgetMapStart + MaxUnique + 1u); + const auto overflowFlags = budgetResolved.map.at(overflowRecord + Abi::SamplerRecordFlags); + require((overflowFlags & Abi::PoisonFlag) != 0u, "sampler table budget: exceeding sampler budget did not poison record"); + require(budgetResolved.poison.at(overflowFlags & ~Abi::PoisonFlag).reason == Abi::PoisonReason::Budget, "sampler table budget: poisoned reason is not Budget"); + + const auto outsideRecord = budgetResolved.map.at(Abi::TableHeader(0) + Abi::TableOutsideCode); + const auto outsideFlags = budgetResolved.map.at(outsideRecord + Abi::SamplerRecordFlags); + require((outsideFlags & Abi::PoisonFlag) != 0u, "sampler table budget: TableOutsideCode did not poison when budget is full"); + require(budgetResolved.poison.at(outsideFlags & ~Abi::PoisonFlag).reason == Abi::PoisonReason::Budget, "sampler table budget: TableOutsideCode poisoned reason is not Budget"); +} + void verifyDynamicStorageMipChain() { using namespace ShaderRecompiler; ImageResource image{}; @@ -7328,6 +7407,7 @@ int main(int argc, char** argv) { verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); verifyVolumeBiasTableRejection(); + verifySamplerTableDeduplication(); verifyDynamicStorageMipChain(); verifySrgbDepthCompareModes(); verifyQuadMaskNeighbourReads(); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 5b869a4c9e..1d781cfdea 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -633,6 +633,7 @@ private: struct WordsIndexTag {}; struct ElementIndexTag {}; +struct SamplerIndexTag {}; struct PoisonIndexTag {}; struct TableRecordsTag {}; @@ -1654,6 +1655,7 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i return Abi::PoisonCode(*index); }; HashScratchIndex elementIndex{threadScratch()}; + HashScratchIndex samplerIndex{threadScratch()}; std::vector directSamplers(info.images.size(), 0u); for (const auto& pair : info.sampledPairs) { if (info.samplers.at(pair.sampler).table == NoTable) directSamplers.at(pair.image) |= 1u << pair.sampler; @@ -1711,19 +1713,29 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Unnormalized, &words); } else if (((words.dwords[0] >> 29u) & 3u) != 0u) { record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Reduction, &words); - } else if (out.samplers.size() + Abi::SamplerEntryElements > Abi::SamplerElementBudget) { - record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Budget, &words); } else { - record[Abi::SamplerRecordElement] = static_cast(out.samplers.size()); - for (std::uint32_t variant = 0; variant < 2u; variant++) { - const bool compare = variant == 1u; - for (std::uint32_t point = 0; point < 2u; point++) { - ImageTableSamplerElement element; - std::copy_n(words.dwords.begin(), 4u, element.words.begin()); - element.compare = compare; - element.point = point != 0u; - out.samplers.push_back(element); + const auto hash = hashWords(std::span(words.dwords.data(), 4u), 0); + auto index = samplerIndex.Find(hash, [&](std::uint32_t known) { + return std::equal(words.dwords.begin(), words.dwords.begin() + 4u, out.samplers[known].words.begin()); + }); + if (!index.has_value() && out.samplers.size() + Abi::SamplerEntryElements > Abi::SamplerElementBudget) { + record[Abi::SamplerRecordFlags] = poison(resource, PoisonReason::Budget, &words); + } else { + if (!index.has_value()) { + index = static_cast(out.samplers.size()); + for (std::uint32_t variant = 0; variant < 2u; variant++) { + const bool compare = variant == 1u; + for (std::uint32_t point = 0; point < 2u; point++) { + ImageTableSamplerElement element; + std::copy_n(words.dwords.begin(), 4u, element.words.begin()); + element.compare = compare; + element.point = point != 0u; + out.samplers.push_back(element); + } + } + samplerIndex.Insert(hash, *index); } + record[Abi::SamplerRecordElement] = *index; } } const auto offset = static_cast(out.map.size()); From c6f44a028a426db11bc77a5a903388e08eaa93f4 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 19 Oct 2026 19:01:53 +0400 Subject: [PATCH 004/318] feat(libkernel): back JIT shared memory with an unlinked temporary file on macOS --- core/libs/prx/libkernel/DirectMemory/Jit.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/Jit.cpp b/core/libs/prx/libkernel/DirectMemory/Jit.cpp index fb3ad0405c..968a36641d 100644 --- a/core/libs/prx/libkernel/DirectMemory/Jit.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Jit.cpp @@ -26,6 +26,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #endif namespace { @@ -140,8 +143,19 @@ int DuplicateDescriptor(int descriptor) { } #else int CreateBacking(std::size_t bytes, int* descriptor, std::shared_ptr* object) { +#ifdef __APPLE__ + auto path = (std::filesystem::temp_directory_path() / "anyps5-jit-XXXXXX").string(); + const int file = mkstemp(path.data()); + if (file < 0) return HostFailure("create JIT shared memory", errno); + if (unlink(path.c_str()) != 0 || fcntl(file, F_SETFD, FD_CLOEXEC) != 0) { + const int error = errno; + ::close(file); + return HostFailure("create JIT shared memory", error); + } +#else const int file = memfd_create("anyps5 jit shared memory", MFD_CLOEXEC); if (file < 0) return HostFailure("create JIT shared memory", errno); +#endif const bool representable = bytes <= static_cast(std::numeric_limits::max()); if (!representable || ftruncate(file, static_cast(bytes)) != 0) { const int error = !representable || errno == EFBIG ? ENOMEM : errno; From ca00680339093340e8a0a860cc91772614cf8a51 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 19 Oct 2026 19:01:53 +0400 Subject: [PATCH 005/318] fix(libkernel): make JIT shared memory views executable after mapping them on macOS --- core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 8d5b7996ad..c4f5dbfe29 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -1150,6 +1150,9 @@ int DoMapShared(void** addr, size_t len, int prot, HostSharedMemory object) { try { #ifdef _WIN32 GuestArena::GuestArenaMap_nid_postfix(mapped, len, object, 0, WinProtFromPosix(nativeProtection)); +#elif defined(__APPLE__) + if (::mmap(mapped, len, nativeProtection & ~PROT_EXEC, MAP_SHARED | MAP_FIXED, object, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map shared memory"); + if ((nativeProtection & PROT_EXEC) != 0 && ::mprotect(mapped, len, nativeProtection) != 0) throw std::system_error(errno, std::generic_category(), "make shared memory executable"); #else if (::mmap(mapped, len, nativeProtection, MAP_SHARED | MAP_FIXED, object, 0) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map shared memory"); #endif From 4e5dba2db6ca340c9160e5d686b4b624ce568398 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 19 Oct 2026 19:01:53 +0400 Subject: [PATCH 006/318] test(libkernel): run the JIT shared memory test on macOS --- core/libs/tests/GuestJitSharedMemory.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestJitSharedMemory.cpp b/core/libs/tests/GuestJitSharedMemory.cpp index 66ed8a4838..ea38bffa71 100644 --- a/core/libs/tests/GuestJitSharedMemory.cpp +++ b/core/libs/tests/GuestJitSharedMemory.cpp @@ -187,6 +187,8 @@ int main() { WriteThroughAliasRunsThroughExecutableView(); OneViewWritesAndRuns(); Dup2KeepsTheDescriptorTableInStep(); +#ifndef __APPLE__ MmapOfTheDescriptorSharesTheObject(); +#endif InvalidArgumentsAreRejected(); } From 8a5e742d9dff2e157622c62cabe84da9d1ebc458 Mon Sep 17 00:00:00 2001 From: David Date: Mon, 19 Oct 2026 19:01:53 +0400 Subject: [PATCH 007/318] docs(debt): record the macOS JIT shared memory backing and its Rosetta limits --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6ccdf90aa5..eedb33cbb4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -114,6 +114,7 @@ - [sceKernelMprotect, sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) over a range with a gap that is not registered in the allocation tracker protect the registered parts and skip the gap, and return 0, as FreeBSD 11.4's [`vm_map_protect`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/vm/vm_map.c) does (it has no check for holes); a range whose start is unmapped (also when later pages are mapped) returns `SCE_KERNEL_ERROR_EINVAL` (`mprotect`: `EINVAL`) because an existing test pins that for an unmapped start; FreeBSD answers 0 there too (`vm_map_protect` takes the next entry), so this errno is not taken from the source and was not changed. `sceKernelMprotect` threw for a gap before, which ended the title. Every part is checked for a GPU or image pin before the first one is protected; the parts are then protected one after the other, so a host protection failure in a later part leaves the earlier parts changed (FreeBSD's `vm_map_protect` cannot fail in its second pass). The behaviour on the console is not known. None of this was measured on a console. - [sceKernelMprotect protection bits](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - bits outside `0x3F7` are dropped instead of refused, as a PS5 (firmware 13.60) drops them: Onimusha: Way of the Sword (PPSA27836 01.000.001) maps direct memory with `0xf2` and then protects it with `0xfffffff2`, which the console reports as `0xf3` and then `0x3f3`. In a payload process the console also drops the CPU execute bit (`0x4`) of flexible, direct and JIT memory, and POSIX `mprotect` with write and execute reports write only (`0x2`). A title process may keep execute, so it is kept here. The map calls and `sceKernelMtypeprotect` refuse undefined bits on the console with `EINVAL` (`0x80020016`), and the direct map calls refuse execute; here both throw for undefined bits, and the direct map calls map execute - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors maps the object as a [regular file](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp), so its views and the JIT views see each other's writes. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws +- [JIT shared memory on macOS](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - the object is an unlinked `mkstemp` file in the temporary directory, as `shm_open` makes its anonymous objects. macOS refuses an executable shared file mapping (`EPERM`), so an executable view is mapped without execute and then given it with `mprotect`. Under Rosetta 2 (Apple M2, macOS 26), code rewritten through one view after it ran from another keeps its old translation until the executable view's protection changes; first writes and a single read-write-execute view, as XPSemu PPSA97358 and VLC PPSA85300 map, are seen. `mmap` of a JIT descriptor is not supported, as file-backed guest `mmap` throws on macOS. - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus `EVFILT_READ` on the read end of a pipe (see the kevent on a pipe entry) and the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters and a read event on the host stream on descriptor 0, which never triggers, as `select` never reports descriptor 0 readable; PPSA34015 registers descriptor 0 for reading in a new kqueue at start-up and does not check the result; other filters, `EV_CLEAR` on a socket or a pipe, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro) and any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) throw. `EV_DISABLE`, `EV_DISPATCH` and `EV_FORCEONESHOT` follow kern_event.c: a disabled knote stays registered, stops polling or firing and is not reported, a trigger raised while it is disabled is kept and reported after `EV_ENABLE`, where `knote` would drop it, `EV_DISPATCH` reports once and then disables the knote, and `EV_FORCEONESHOT` is ignored on a new knote while on an existing one it sets `EV_ONESHOT` and fires the knote; a negative `nevents` fails with `EINVAL`. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop. `NOTE_LOWAT` on a socket sets the low-water mark as FreeBSD 11's [`filt_soread` and `filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/kern/uipc_socket.c) do, and a later change of the knote replaces it as `kqueue_register` replaces `kn_sdata`: a read event fires once the buffered bytes reach the mark, or at end of file or on a listening socket. A read mark above 1 is taken on stream sockets only: on a datagram socket Linux's `FIONREAD` reports the next datagram where FreeBSD's `sbavail` counts every queued byte, so it throws. A write event takes a mark of 1 only, where the host's writability stands in for one free byte, since the host does not report the send buffer's free space; other write marks and marks below 1, which make FreeBSD's read filter always fire, throw - [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. The three Windows notifications are told apart in the error, which names the `parent`, `entries` or `subdirectories` watch, the path it was given and `GetLastError`, but a `FindFirstChangeNotification` that fails still throws rather than returning an error to the guest: the EVFILT_VNODE attach path is [`vfs_kqfilter`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L4936), which returns `EINVAL`, `ENOMEM` or 0 after `fget` has turned a descriptor that is not open into `EBADF`, and consults no directory permission, so no FreeBSD errno stands for a host that refuses to notify, and what the console does on such a host is not measured. Only the `parent` watch can fail this way in practice: `entries` and `subdirectories` need the `FILE_LIST_DIRECTORY` that opening the watched directory already took. The parent handle produces no hint of its own, as `RunHostWatch` only calls `FindNextChangeNotification` on it, so a later change may leave the watch on its 50 ms poll instead of throwing; that is not done here. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late, and a file deleted while watched, which the POSIX delete of Windows 10 1903 moves into `$Extend\$Deleted` until its last handle closes, reports `NOTE_DELETE` without `NOTE_RENAME` and keeps its handle without a parent directory watch, as that directory cannot be watched; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title (feat(libkernel): implement kevent EV_DISABLE, EV_DISPATCH and EV_FORCEONESHOT) (fix(libkernel): follow kqueue_register for EV_FORCEONESHOT and modify flags) - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. From 12655e4fa5352a6388e5ffbc8d75d93aa7ed4ea5 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:07:48 +0100 Subject: [PATCH 008/318] perf(agc): reuse sampled snapshot byte buffers across cache entry replacement A surface sampled after its storage image flushes is snapshotted, and the next frame's pending image replaces the entry with a view: the 32 MiB guest buffer was freed and re-allocated zero-filled on every alternation, about 3.2 ms per 4K snapshot on the committer. The replaced entry now passes its buffer to its replacement, and a later snapshot of the same size adopts it. ReadTextureSurface overwrites every byte before use. Bendy chapter 1 steady state, RX 7900 GRE, 3840x2160: median frame 92.6 -> 78.0 ms, p99 151.4 -> 87.3 ms, 313 -> 383 presents per 30 s. --- .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 75ab4242bc..6e9632f272 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -119,6 +119,7 @@ struct CachedTexture { // The untrackedKey the snapshot was last read or compared equal at, for a surface outside the // write watch; zero when none was taken. std::array untracked{}; + std::vector carried; }; // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the @@ -772,6 +773,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span recycled; if (auto it = findTexture(cache, key); it != cache.entries.end()) { if (it->source != nullptr) { // The view follows the storage image, whatever the GPU wrote to it since; guest memory @@ -817,10 +819,13 @@ std::shared_ptr cachedTexture(const Context& context, std::spantexture; } } + if (it->bytes.size() == bytes) recycled = std::move(it->bytes); + else if (it->carried.size() == bytes) recycled = std::move(it->carried); eraseTexture(cache, it); ++cache.counts.stale; } - CachedTexture entry{key, address, std::vector(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; + CachedTexture entry{key, address, source != nullptr ? std::vector{} : (recycled.empty() ? std::vector(bytes) : std::move(recycled)), nullptr, *keys, snapshotGeneration}; + if (source != nullptr) entry.carried = std::move(recycled); entry.committed = std::move(committed); if (source != nullptr) { entry.source = source; From 268dfdda102f1f821698f6045629077faa539b11 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 19 Oct 2026 14:10:16 -0400 Subject: [PATCH 009/318] fix(agc): separate pipeline drain and device lock timings --- .../Execution/src/Driver/Packets/PacketExecution.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 077fef550a..c1938a3b5e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -240,8 +240,10 @@ void Driver::execute(const Submission& submission) { continue; } } + timing.Mark("pipeline_prepare"); if (pipelined && DrawPipeline::Queue0().Busy() && !keepsDrawPipeline(packet, header, opcode)) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, header == FlipPacketHeader ? 0xffu : opcode); if (pipelined && predicated && DrawPipeline::Queue0().Busy() && DrawPipeline::Queue0().Overlaps(queue.predication.address, queue.predication.operation == 3 ? 8 : 4)) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); + timing.Mark("pipeline_drain"); std::shared_lock deviceUse(deviceReplacement, std::defer_lock); if (opcode != 0x3c && opcode != 0x93 && header != RenderingWaitPacketHeader && header != FlipPacketHeader) deviceUse.lock(); From 3c905ee2d6500f1eaeb9ea6e27f2a4cb780556ac Mon Sep 17 00:00:00 2001 From: thuanquang Date: Mon, 19 Oct 2026 14:49:27 +0700 Subject: [PATCH 010/318] fix(agc): give a block-copied BC texture an sRGB view for FORCE_DEGAMMA samplers Co-Authored-By: Claude --- .../libSceAgcDriver/Graphics/src/Texture.cpp | 2 ++ .../tests/execution/BcBlockCopy.cpp | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d7a68e3704..4778470b76 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -673,6 +673,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& imageInfo.arrayLayers = copyLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.flags = SrgbTwinFormat(context, blockFormat) != VK_FORMAT_UNDEFINED ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT : 0u; imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; @@ -702,6 +703,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; createFirstLayerView(descriptor, viewInfo); prepareBaseLevelView(descriptor, viewInfo); + prepareSrgbView(descriptor, viewInfo); } catch (...) { release(); throw; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp index 23eeb6dcf7..216bfbc31d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BcBlockCopy.cpp @@ -35,6 +35,7 @@ constexpr std::uint32_t Format32x2Uint = 62; constexpr std::uint32_t Format32x4Uint = 75; constexpr std::uint32_t FormatBc1Unorm = 169; constexpr std::uint32_t FormatBc7Unorm = 181; +constexpr std::uint32_t ForceDegamma = 1u << 20u; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t ClampEdge = 2; constexpr std::size_t BlockBytes = 1u << 20u; @@ -130,8 +131,8 @@ std::array TextureDescriptor(const void* data, const Shape& sh }; } -std::array SamplerDescriptor() { - return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; +std::array SamplerDescriptor(std::uint32_t forced) { + return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u) | forced, 0u, 0u, 0u}; } ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData) { @@ -167,12 +168,12 @@ void Settle(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::uint64_t AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(texels), bytes); } -std::vector Sample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape) { +std::vector Sample(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Shape& shape, std::uint32_t forced = 0u) { std::vector userData(24, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size_bytes())); const auto output = BufferDescriptor(Output.data(), static_cast(Output.size_bytes())); const auto texture = TextureDescriptor(texels, shape); - const auto sampler = SamplerDescriptor(); + const auto sampler = SamplerDescriptor(forced); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); std::copy(texture.begin(), texture.end(), userData.begin() + 8); @@ -189,12 +190,12 @@ std::vector Sample(AgcDriver::VulkanDevice& device, std::uint8_t* return {Output.begin(), Output.end()}; } -std::vector Reference(AgcDriver::VulkanDevice& device, const Case& test, std::uint32_t value) { +std::vector Reference(AgcDriver::VulkanDevice& device, const Case& test, std::uint32_t value, std::uint32_t forced = 0u) { GuestBlock block; auto* texels = block.Data(); Store(device, texels, test, value); Settle(device, texels, SurfaceBytes(texels, test.blocks)); - return Sample(device, texels, test.compressed); + return Sample(device, texels, test.compressed, forced); } void RequireSame(const std::vector& sampled, const std::vector& expected, const std::string& what) { @@ -208,7 +209,11 @@ void PendingBlocksCase(AgcDriver::VulkanDevice& device, const Case& test) { constexpr std::uint32_t Second = 0x9bdf6840u; const auto first = Reference(device, test, First); const auto second = Reference(device, test, Second); + const auto forcedFirst = Reference(device, test, First, ForceDegamma); + const auto forcedSecond = Reference(device, test, Second, ForceDegamma); Require(first != second, test.name + ": the two block patterns sample alike"); + Require(forcedFirst != first && forcedSecond != second, test.name + ": a sampler that forces sRGB decoding samples the stored blocks like a plain one"); + Require(forcedFirst != forcedSecond, test.name + ": the two block patterns sample alike through a sampler that forces sRGB decoding"); GuestBlock block; auto* texels = block.Data(); const auto bytes = SurfaceBytes(texels, test.compressed); @@ -222,6 +227,9 @@ void PendingBlocksCase(AgcDriver::VulkanDevice& device, const Case& test) { RequireSame(pending, second, test.name + ": sample of pending blocks"); Store(device, texels, test, First); RequireSame(Sample(device, texels, test.compressed), first, test.name + ": sample of blocks stored again"); + Store(device, texels, test, Second); + RequireSame(Sample(device, texels, test.compressed, ForceDegamma), forcedSecond, test.name + ": sRGB-forced sample of pending blocks"); + Require(AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), bytes) != nullptr, test.name + ": the sRGB-forced sample stored the pending blocks to guest memory"); } } From 20ddc95c566ef660e07de2161dc68596c355a5fa Mon Sep 17 00:00:00 2001 From: Izamaru Date: Mon, 19 Oct 2026 18:01:31 -0300 Subject: [PATCH 011/318] fix(agc): allocate Windows page permissions on demand --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 11 ++ .../Execution/src/GuestMemory.cpp | 57 +++++-- .../prx/libSceAgcDriver/tests/PageStates.cpp | 145 ++++++++++++++++++ 3 files changed, 204 insertions(+), 9 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/PageStates.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d4b9a576e1..139b4c5fa1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -629,6 +629,17 @@ target_link_libraries(agc_shared_write_tests PRIVATE libSceAgcDriver libkernel l set_target_properties(agc_shared_write_tests PROPERTIES CXX_EXTENSIONS OFF) configure_windows_unwind(agc_shared_write_tests) +if(BUILD_TESTING AND WIN32) + add_test_executable(agc_page_state_tests tests/PageStates.cpp) + target_include_directories(agc_page_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_page_state_tests PRIVATE libSceAgcDriver libc) + configure_windows_unwind(agc_page_state_tests) + add_test(NAME agc_page_states COMMAND agc_page_state_tests) + add_test(NAME agc_page_state_metadata COMMAND agc_page_state_tests --metadata) + set_tests_properties(agc_page_states agc_page_state_metadata PROPERTIES + TIMEOUT 30 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") +endif() + add_test_executable(agc_write_tracking_tests EXCLUDE_FROM_ALL tests/WriteTracking.cpp Execution/src/GuestMemory.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 90afd6a816..23f2e4ce41 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -367,18 +368,38 @@ struct PageSpan { std::size_t size = 0; // Per page: access flags, zero while unknown. std::uint8_t* pages = nullptr; + static constexpr std::size_t ChunkEntries = 65536; + struct alignas(8) Chunk { std::array entries{}; }; + std::atomic* chunks = nullptr; + + std::uint8_t* chunkData(std::size_t index, bool create) const { + auto* chunk = chunks[index].load(std::memory_order_acquire); + if (chunk == nullptr && create) { + auto fresh = std::unique_ptr(new (std::nothrow) Chunk{}); + if (fresh != nullptr && chunks[index].compare_exchange_strong(chunk, fresh.get(), std::memory_order_acq_rel, std::memory_order_acquire)) chunk = fresh.release(); + } + return chunk == nullptr ? nullptr : chunk->entries.data(); + } + + std::uint8_t* entry(std::size_t index, bool create) const { + if (chunks == nullptr) return pages + index; + auto* data = chunkData(index / ChunkEntries, create); + return data == nullptr ? nullptr : data + index % ChunkEntries; + } - // Reserves and commits the table; untouched table pages cost nothing until a guest page is recorded. bool allocate() { if (size == 0) return false; const auto count = (size / PageBytes + 8) & ~static_cast(7); #ifdef _WIN32 - pages = static_cast(VirtualAlloc(nullptr, count, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + const auto chunkCount = (count + ChunkEntries - 1) / ChunkEntries; + chunks = new (std::nothrow) std::atomic[chunkCount]{}; + if (chunks == nullptr) size = 0; + return chunks != nullptr; #else pages = static_cast(std::calloc(count, 1)); -#endif if (pages == nullptr) size = 0; return pages != nullptr; +#endif } bool covers(std::uintptr_t address) const { @@ -386,11 +407,23 @@ struct PageSpan { } std::uint8_t load(std::uintptr_t address) const { - return std::atomic_ref(const_cast(pages[(address - base) / PageBytes])).load(std::memory_order_relaxed); + auto* value = entry((address - base) / PageBytes, false); + return value == nullptr ? 0 : std::atomic_ref(*value).load(std::memory_order_relaxed); } void store(std::uintptr_t address, std::uint8_t value) { - std::atomic_ref(pages[(address - base) / PageBytes]).store(value, std::memory_order_relaxed); + if (auto* at = entry((address - base) / PageBytes, value != 0 && value != PageInaccessible)) std::atomic_ref(*at).store(value, std::memory_order_relaxed); + } + + void store(std::uintptr_t first, std::uintptr_t last, std::uint8_t value) { + auto index = (first - base) / PageBytes; + const auto stop = (last - 1 - base) / PageBytes + 1; + while (index < stop) { + const auto next = std::min(stop, (index / ChunkEntries + 1) * ChunkEntries); + if (auto* at = entry(index, value != 0 && value != PageInaccessible)) { + for (; index < next; ++index, ++at) std::atomic_ref(*at).store(value, std::memory_order_relaxed); + } else index = next; + } } // End of the run of pages equal to `value` that starts at `address`, at most `limit`. @@ -399,11 +432,17 @@ struct PageSpan { const auto stop = std::min((std::min(limit, base + size) - base + PageBytes - 1) / PageBytes, size / PageBytes); const std::uint64_t wide = 0x0101010101010101ull * value; while (index < stop) { - if (index % 8 == 0 && index + 8 <= stop && std::atomic_ref(*reinterpret_cast(const_cast(pages + index))).load(std::memory_order_relaxed) == wide) { + auto* at = entry(index, false); + if (at == nullptr) { + if (value != 0) break; + index = std::min(stop, (index / ChunkEntries + 1) * ChunkEntries); + continue; + } + if (index % 8 == 0 && index + 8 <= stop && std::atomic_ref(*reinterpret_cast(at)).load(std::memory_order_relaxed) == wide) { index += 8; continue; } - if (std::atomic_ref(const_cast(pages[index])).load(std::memory_order_relaxed) != value) break; + if (std::atomic_ref(*at).load(std::memory_order_relaxed) != value) break; ++index; } return std::min(limit, base + index * PageBytes); @@ -413,7 +452,7 @@ struct PageSpan { if (size == 0 || bytes == 0 || address >= base + size || address + bytes <= base) return; const auto first = (std::max(address, base) - base) / PageBytes; const auto last = (std::min(address + bytes, base + size) - 1 - base) / PageBytes; - for (auto page = first; page <= last; ++page) std::atomic_ref(pages[page]).store(0, std::memory_order_relaxed); + store(base + first * PageBytes, base + (last + 1) * PageBytes, 0); } }; @@ -444,7 +483,7 @@ struct PageStates { const std::uint8_t value = readable ? static_cast(PageReadable | (writable ? PageWritable : 0u)) : PageInaccessible; const auto first = std::max(base, span->base); const auto last = std::min(regionEnd, span->base + span->size); - for (auto at = first; at < last; at += PageBytes) span->store(at, value); + span->store(first, last, value); if (GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation || accessSerial.load(std::memory_order_acquire) != access) span->forget(first, static_cast(last - first)); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/PageStates.cpp b/core/libs/prx/libSceAgcDriver/tests/PageStates.cpp new file mode 100644 index 0000000000..fb92db485c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PageStates.cpp @@ -0,0 +1,145 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +namespace { +using namespace AgcDriver::GuestMemory; +constexpr std::size_t Unit = 65536; +constexpr std::size_t ChunkGuestBytes = 256ull << 20u; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +class Range { +public: + explicit Range(std::size_t bytes) : bytes(bytes) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, ChunkGuestBytes)); + Require(data != nullptr, "cannot reserve page-state fixture"); + } + + ~Range() { + { + GuestAllocations::Mutation mutation; + for (const auto offset : committed) mutation.Remove(data + offset); + } + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } + + std::byte* Commit(std::size_t offset) { + auto* at = data + offset; + GuestArena::GuestArenaCommit_nid_postfix(at, Unit, PAGE_READWRITE, Unit); + std::memset(at, 0x5b, Unit); + GuestAllocations::Mutation mutation; + mutation.Add(at, Unit, true, true, true); + committed.push_back(offset); + return at; + } + + std::byte* Data() const { return data; } + +private: + std::byte* data = nullptr; + std::size_t bytes; + std::vector committed; +}; + +std::size_t PrivateBytes() { + PROCESS_MEMORY_COUNTERS_EX counters{}; + Require(K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&counters), sizeof(counters)), "cannot query page-state fixture private commit"); + return counters.PrivateUsage; +} + +void CheckMetadata() { + Range range(3 * ChunkGuestBytes); + auto* data = range.Commit(0); + const auto before = PrivateBytes(); + Require(Accessible(data, Unit, true), "committed fixture is inaccessible"); + const auto after = PrivateBytes(); + const auto added = after > before ? after - before : 0; + std::printf("page-permission initialization: %zu bytes of private commit\n", added); + Require(added < (16ull << 20u), "page permissions committed a dense arena table"); + const auto queried = PrivateBytes(); + Require(!Accessible(range.Data() + 2 * ChunkGuestBytes, Unit), "reserved fixture tail is accessible"); + Require(PrivateBytes() < queried + (4ull << 20u), "querying reserved pages allocated untouched metadata chunks"); +} + +void CheckBoundaries() { + constexpr auto span = 3 * ChunkGuestBytes; + Range range(span); + const std::array offsets{0, ChunkGuestBytes - Unit, ChunkGuestBytes, 2 * ChunkGuestBytes + Unit}; + for (const auto offset : offsets) range.Commit(offset); + auto* block = range.Data(); + std::atomic begin{false}; + std::atomic errors{0}; + std::vector readers; + for (unsigned reader = 0; reader < 8; ++reader) { + readers.emplace_back([&] { + while (!begin.load(std::memory_order_acquire)) std::this_thread::yield(); + for (unsigned round = 0; round < 500; ++round) { + if (!Accessible(block + offsets.back(), Unit, true)) errors.fetch_add(1, std::memory_order_relaxed); + } + }); + } + begin.store(true, std::memory_order_release); + for (auto& reader : readers) reader.join(); + Require(errors.load() == 0, "concurrent page-state publication changed access"); + for (const auto offset : offsets) { + std::array copy{}; + Read(reinterpret_cast(block + offset), copy); + Require(std::all_of(copy.begin(), copy.end(), [](std::byte value) { return value == std::byte{0x5b}; }), "page cache changed guest bytes"); + } + Require(Accessible(block + ChunkGuestBytes - Unit, 2 * Unit, true), "adjacent chunks lost their boundary pages"); + const auto described = DescribeCommitted(reinterpret_cast(block), span, true); + std::uint64_t total = 0; + for (const auto& [first, last] : described.ranges) total += last - first; + Require(!described.whole && total == offsets.size() * Unit, "reserved holes were reported committed"); + auto* changed = block + offsets[1]; + const auto protect = [&](bool writable) { + GuestAllocations::Mutation mutation; + mutation.Protect(changed, Unit, true, writable, true, [&] { + const DWORD protection = writable ? PAGE_READWRITE : PAGE_READONLY; + DWORD previous = 0; + Require(VirtualProtect(changed, Unit, protection, &previous) != 0, "cannot protect page-state fixture"); + GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast(changed), Unit, protection); + }); + }; + protect(false); + Require(Accessible(changed, Unit) && !Accessible(changed, Unit, true), "protection invalidation retained writable state"); + protect(true); + Require(Accessible(changed, Unit, true), "restored writable state was not discovered"); + GuestArena::GuestArenaReset_nid_postfix(block, Unit); + GuestAllocations::GuestAllocationsInvalidate_nid_postfix(reinterpret_cast(block), Unit); + Require(!Accessible(block, Unit), "decommitted pages remained readable"); + GuestArena::GuestArenaCommit_nid_postfix(block, Unit, PAGE_READWRITE, Unit); + Require(Accessible(block, Unit, true), "access observer did not invalidate inaccessible state after commit"); + std::puts("page-state boundaries, publication, holes and invalidation passed"); +} +} + +int main(int argc, char** argv) { + try { + Require(GuestArena::GuestArenaAvailable_nid_postfix(), "page-state fixture needs the guest arena"); + if (argc == 2 && std::string_view(argv[1]) == "--metadata") CheckMetadata(); + else CheckBoundaries(); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} From 11cc05929f6f552a900dd7d7a1934e0f34741af2 Mon Sep 17 00:00:00 2001 From: ebosido444-ai Date: Tue, 20 Oct 2026 00:59:30 +0300 Subject: [PATCH 012/318] fix(agc): place a layered depth storage view at the guest's layer pitch A 2D-array storage view over a depth surface is served as one storage image with a copy per layer, but each layer's depth surface was looked up at layer * DepthSliceBytes, and any other guest layer pitch was refused outright even though the sampling path already reads each layer at geometry.GuestLayerOffset(layer). The lookup now uses the descriptor geometry's layer pitch, kept with the layered storage, and the refusal is gone: a tight linear depth array whose pitch is under the 64 KiB-aligned slice size is the ordinary case, not an error. --- .../libSceAgcDriver/Graphics/src/DepthSurface.cpp | 10 +++------- .../libSceAgcDriver/tests/execution/DepthStorage.cpp | 12 ++++++++++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index b28491b129..a69d2a538d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -1151,6 +1151,7 @@ class LayeredDepthStorage { public: LayeredDepthStorage(const Context& context, const GuestTextureResource& resource, const DepthTarget& base, std::uint32_t bytesPerTexel, std::uint32_t firstLayer, std::uint32_t layers) : context(context), base(base), extent(base.extent), bytesPerTexel(bytesPerTexel), firstLayer(firstLayer), layers(layers) { const auto bytes = DescribeSurface(resource).guestBytes; + layerPitchBytes = DescribeSurface(resource).GuestLayerOffset(1); StorageTexture::FlushPending(resource.baseAddress, static_cast(bytes), nullptr, "storage image creation", PublishScope::None); storage = StorageTexture::Create(context, *context.detiler, resource, 0); Require(storage->ImageLayers() == layers, "a layered depth storage view's image does not hold its layers"); @@ -1204,6 +1205,7 @@ public: const std::uint32_t bytesPerTexel; const std::uint32_t firstLayer; const std::uint32_t layers; + std::uint64_t layerPitchBytes = 0; std::shared_ptr storage; private: @@ -1947,7 +1949,7 @@ std::vector layeredSlices(const Context& context, const LayeredDep std::vector slices; const auto& list = surfaces(); for (std::uint32_t layer = storage.firstLayer; layer < storage.layers; ++layer) { - const auto address = base.address + static_cast(layer) * DepthSliceBytes(base.extent, storage.bytesPerTexel); + const auto address = base.address + static_cast(layer) * storage.layerPitchBytes; const auto slice = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) { return surface->context.device == context.device && !surface->retired && surface->target.address == address && surface->target.extent.width == base.extent.width && surface->target.extent.height == base.extent.height && surface->target.format == base.format && surface->target.samples == 1; }); if (slice != list.rend()) slices.push_back({slice->get(), layer}); } @@ -1960,12 +1962,6 @@ std::shared_ptr layeredDepthStorage(const Context& context, Dept const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT; const auto bytesPerTexel = d16 ? 2u : 4u; surface.RequireStorable(resource); - const auto geometry = DescribeSurface(resource); - if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) { - char text[256]; - std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth planes of depth surface 0x%llx with a layer pitch of 0x%llx bytes, not the depth slice size 0x%llx, is not implemented", static_cast(target.address), static_cast(geometry.GuestLayerOffset(1)), static_cast(DepthSliceBytes(target.extent, bytesPerTexel))); - throw std::runtime_error(text); - } auto& entry = layeredStorages()[{context.device, resource.baseAddress, resource.width, resource.height, layers, resource.format, static_cast(resource.tileMode), static_cast(resource.dimension), resource.baseArray}]; if (entry == nullptr) entry = std::make_unique(context, resource, target, bytesPerTexel, resource.baseArray, layers); else static_cast(entry->storage->Refresh()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index 3ab804d307..d5a4fac983 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -250,9 +250,17 @@ void Run(Context context, std::uint8_t* block) { layeredStencil.depthOrLastArray = 1; ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredStencil, 0)); }, "storage image access to depth/stencil surface", "a two-layer 2D array view of the stencil plane was accepted"); layeredDepth.depthOrLastArray = 1; - ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a two-layer 2D array view of the depth plane with a layer pitch other than the depth slice size was accepted"); + const auto layerPitch = static_cast(DescribeSurface(layeredDepth).GuestLayerOffset(1)); + const auto sliceBytes = DepthSliceBytes({Side, Side}, 4u); + Require(layerPitch != sliceBytes, "the test's layered depth view has the depth slice size as its layer pitch"); + const auto layered = DepthSurfaceStorage(context, {}, layeredDepth, 0); + Require(layered != nullptr && layered->ImageLayers() == 2, "a two-layer 2D array view over the depth plane's guest layer pitch was not taken as a storage image"); + submit(recorder); + readBack(*layered); + Require(layerPitch + sliceBytes > BlockBytes || CountFloats(block + layerPitch, 0.5f) == Side * Side, "the two-layer 2D array view did not place its layers at the guest layer pitch"); + Require(CountFloats(block, 0.5f) == Side * Side, "the two-layer 2D array view did not receive the layer the depth surface holds"); layeredDepth.baseArray = 1; - ExpectFailure([&] { static_cast(DepthSurfaceStorage(context, {}, layeredDepth, 0)); }, "with a layer pitch of", "a 2D array view of layer 1 of the depth plane with a layer pitch other than the depth slice size was accepted"); + Require(DepthSurfaceStorage(context, {}, layeredDepth, 0) != nullptr, "a 2D array view from layer 1 of the depth plane was not taken as a storage image"); const auto own = StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float), 0); const std::array eighth{std::bit_cast(0.125f), std::bit_cast(0.125f), std::bit_cast(0.125f), std::bit_cast(0.125f)}; Require(own->FillClear(eighth, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the storage image could not be filled"); From 7372189a7162e83fa57c86332bff32a65b36c071 Mon Sep 17 00:00:00 2001 From: raghav314 Date: Tue, 20 Oct 2026 02:40:00 +0530 Subject: [PATCH 013/318] fix(agc): write-watch the AGC driver memory on Linux On Linux the 2 MiB AGC driver memory at 0xfe0000000 is a plain mapping in the arena's system-reserved hole, outside the write watch, while Windows commits it inside the write-watched arena. Every address-based build maps the registered ranges, so when the host import budget refuses the range it gets no heap mirror and is copied on the CPU for every build. The range is now registered with the write watch, so a refused import is served from a mirror refreshed by the blocks the CPU writes. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp | 4 ++++ .../libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index b70750163e..16d0d9ed78 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -14,6 +14,7 @@ #include "prx/libc/include/GuestAllocations.hpp" #include "ShaderDiskCache.hpp" #include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include #include #include @@ -87,6 +88,9 @@ void MapDriverMemory() { auto* slot = reinterpret_cast(FsTableAddress); slot[0] = (FsRoutineTableAddress & 0xffffffffffffull) | 16ull << 48u; slot[1] = FsTableDword3 << 32u | FsRoutineRecords; +#ifndef _WIN32 + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(mapped, DriverMemoryBytes); +#endif GuestAllocations::Mutation mutation; mutation.Add(mapped, DriverMemoryBytes, true, true, true); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp index 923b3b7212..15e527edc4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" #include "SceShaders.hpp" #include "VulkanTestDevice.hpp" #include @@ -20,6 +21,8 @@ constexpr std::uint32_t Threads = 64; constexpr std::uint32_t Untouched = 0xcafebabeu; constexpr std::uint32_t Marker = 0x55u; constexpr std::uint64_t FsTable = 0xfe0040000ull; +constexpr std::uint64_t DriverMemory = 0xfe0000000ull; +constexpr std::size_t DriverMemoryBytes = 2u << 20u; alignas(256) constexpr std::array Caller{ 0x34040082u, 0x7e0202ffu, 0x00000055u, 0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080235u, 0xfa000000u, 0xbf8cc07fu, 0xf4240384u, @@ -83,6 +86,9 @@ void CheckTable() { Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast(table[2]), sizeof(routine)), "the FS routine is not readable"); std::memcpy(routine.data(), reinterpret_cast(table[2]), sizeof(routine)); Require(routine[0] == 0xbe80200eu, "the FS routine does not start with s_setpc_b64 s[14:15]"); +#ifndef _WIN32 + if (GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix()) Require(AgcDriver::GuestMemory::Watched(DriverMemory, DriverMemoryBytes), "the AGC driver memory at 0xfe0000000 is not write-watched, so a refused import copies it on every address-based build"); +#endif } } From 87ee5e245585e6179877b6c949e0a6033c5d7c3b Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Tue, 20 Oct 2026 01:37:13 +0200 Subject: [PATCH 014/318] fix(agc): drain a label while a copied write-back is pending A label recorded while a copied writer is listed went to the GPU and landed before the writer's CPU write-back ran at the batch's reap, so a title that rewrote the written region after polling the label had its bytes overwritten by the late write-back. The label now takes the drain path, which runs the write-back first and stores the label on the CPU. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit ec3dc6964172720e8364f6153ba34086cd581740) --- .../Execution/src/VulkanDevice.cpp | 1 + .../execution/CopiedWriterLabelOrder.cpp | 167 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 169 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index aedf2f6afb..76a12cd929 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2269,6 +2269,7 @@ int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::spancopiedWriters->empty() || !Graphics::DrawCopiedWriters()->empty()) return 2; if (gateAll ? recorder.HasCompletions() : state->CopiedWriterOverlaps(address, bytes.size())) { // A copied buffer's write-back is still to run: the store must land after it. It is stored // by a completion action of the batch (a CPU memcpy when the batch is reaped) instead of diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp new file mode 100644 index 0000000000..3f7a722c88 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp @@ -0,0 +1,167 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Recorder; +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Threads = 32; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t RecordBytes = 16; +constexpr std::size_t StoredOffset = 8; +constexpr std::size_t CopiedRecordOffset = 0x1004; +constexpr std::size_t DirectRecordOffset = 0x1000; +constexpr std::uint32_t CpuValue = 0xc0001000u; +constexpr std::uint32_t GpuValue = 1; +constexpr std::uint32_t LabelValue = 1; + +alignas(256) constexpr std::array StoreCode{ + 0x7e000281u, 0x7e020288u, 0xe0701000u, 0x80000001u, 0xbf810000u, 0xbf810000u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "copied writer label order: cannot allocate a guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + volatile std::uint32_t* Word(std::size_t offset) const { return reinterpret_cast(block + offset); } + std::uint64_t Address() const { return static_cast(reinterpret_cast(block)); } + +private: + std::uint8_t* block = nullptr; +}; + +std::string Hex(std::uint32_t value) { + char text[16]; + std::snprintf(text, sizeof(text), "0x%08x", value); + return text; +} + +void RecordDispatch(AgcDriver::VulkanDevice& device, std::uint64_t record) { + std::vector userData(16, 0u); + const std::array descriptor{static_cast(record), static_cast((record >> 32u) & 0xffffu), RecordBytes, 0x31016facu}; + std::copy(descriptor.begin(), descriptor.end(), userData.begin()); + const std::span code(StoreCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderRecompiler::ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); +} + +bool LabelPass(AgcDriver::VulkanDevice& device, const GuestBlock& records, std::size_t recordOffset, const GuestBlock& labels, std::size_t labelOffset, std::uint64_t stamp, std::uint64_t writeBacks, int expectedReason, const std::string& what) { + volatile std::uint32_t* stored = records.Word(recordOffset + StoredOffset); + volatile std::uint32_t* label = labels.Word(labelOffset); + *stored = CpuValue; + *label = 0; + RecordDispatch(device, records.Address() + recordOffset); + const auto pending = Recorder::PendingWriteBackCompletions(); + Require(pending == writeBacks, what + ": " + std::to_string(pending) + " write-back completions are pending after the dispatch, expected " + std::to_string(writeBacks) + " (the test needs APS5_CPU_COPIES=1 and a storage buffer offset alignment above 4)"); + const std::array bytes{std::byte{LabelValue}, std::byte{0}, std::byte{0}, std::byte{0}}; + int reason = 0; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + reason = device.WriteLabelOnGpu(labels.Address() + labelOffset, bytes, stamp, 0, false); + if (reason == 3) { + device.WaitIdle(); + return false; + } + Require(reason == expectedReason, what + ": the label after the dispatch was handled with reason " + std::to_string(reason) + ", expected " + std::to_string(expectedReason)); + if (reason >= 1 && reason <= 4) { + if (reason != 1) device.WaitIdle(); + AgcDriver::GuestMemory::Write(labels.Address() + labelOffset, bytes, 4); + } else { + device.SubmitRecorded(true); + } + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (*label != LabelValue) { + Require(std::chrono::steady_clock::now() < deadline, what + ": the label never landed"); + std::this_thread::sleep_for(std::chrono::microseconds(100)); + } + const std::uint32_t seen = *stored; + Require(seen == GpuValue, what + ": the label is visible but the record holds " + Hex(seen) + ", the dispatch stored " + Hex(GpuValue)); + *stored = CpuValue; + device.WaitIdle(); + const std::uint32_t kept = *stored; + Require(kept == CpuValue, what + ": the record holds " + Hex(kept) + " after the drain, a write-back landed over the value the CPU wrote after the label (" + Hex(CpuValue) + ")"); + Require(Recorder::PendingWriteBackCompletions() == 0, what + ": write-back completions outlived the drain"); + return true; +} + +} + +int main() { + try { +#ifdef _WIN32 + _putenv_s("APS5_CPU_COPIES", "1"); +#else + setenv("APS5_CPU_COPIES", "1", 1); +#endif + const GuestBlock copied; + const GuestBlock direct; + const GuestBlock labels; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!LabelPass(*device, copied, CopiedRecordOffset, labels, 0x2000, 1, 1, 2, "copied writer") || !LabelPass(*device, direct, DirectRecordOffset, labels, 0x3000, 2, 0, 0, "direct writer")) { + std::puts("skipped, the device imports no host memory"); + return VulkanTestSkipped; + } + std::puts("copied writer label order tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 259f6530b9..79655753f4 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -113,6 +113,7 @@ - A [COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. - A [COND_INDIRECT_BUFFER](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (`sceAgcCbBranch`) runs only with the always-pass compare function; a comparison or a predicated packet throws. Modes 1 and 2 run the first buffer, assumed to be the if-then and if-then-else modes of AMD's PM4 packet. [sceAgcCbBranch](../../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) does not check the compare address of an always-pass branch or the address of an empty buffer, as PPSA14632 passes an uninitialised compare address and a null else buffer of 0 dwords. - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. +- A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) recorded while a copied writer is listed (a dispatch or draw whose written buffer the device holds as a CPU copy, written back to guest memory when its batch is reaped) waits for all recorded work, which runs the write-back, and is then written by the CPU: on the console every earlier store is in memory before a later label of the same queue is visible, and a title that rewrites the written region after polling the label would otherwise have the bytes overwritten by the late write-back (Gang Beasts, CUSA04865 through #6977, rebuilt its command buffer over such a region). The stall is paid once per label while such a write-back is pending; written buffers the device imports in place bind directly and pay nothing. - [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. - [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown - [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. From 5450c93e936c4a2e17a922b76c01ee75aaa3546e Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 14:28:09 +0700 Subject: [PATCH 015/318] fix(agc): sample 8-bit SNORM textures through FORCE_DEGAMMA as their sRGB format Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 5 ++++- core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp | 1 + .../tests/execution/ImageSampleForceDegamma.cpp | 7 ++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index d7a68e3704..9696cc95ac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1291,7 +1291,10 @@ VkFormat SrgbTwinFormat(const Context& context, VkFormat format) { switch (format) { case VK_FORMAT_R8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB; case VK_FORMAT_R8G8_UNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB; - case VK_FORMAT_R8G8B8A8_UNORM: return VK_FORMAT_R8G8B8A8_SRGB; + case VK_FORMAT_R8_SNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8_SRGB; + case VK_FORMAT_R8G8_SNORM: return decoded(ShaderRecompiler::IrBufferFormat::Format8_8Srgb) ? VK_FORMAT_UNDEFINED : VK_FORMAT_R8G8_SRGB; + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_R8G8B8A8_SNORM: return VK_FORMAT_R8G8B8A8_SRGB; case VK_FORMAT_B8G8R8A8_UNORM: return VK_FORMAT_B8G8R8A8_SRGB; case VK_FORMAT_A8B8G8R8_UNORM_PACK32: return VK_FORMAT_A8B8G8R8_SRGB_PACK32; case VK_FORMAT_BC1_RGB_UNORM_BLOCK: return VK_FORMAT_BC1_RGB_SRGB_BLOCK; diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 31ff43033e..b496d90902 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -63,6 +63,7 @@ void srgbTwinTests() { Require(SrgbTwinFormat(context, VK_FORMAT_R8_UNORM) == VK_FORMAT_UNDEFINED, "a device that decodes 8_SRGB in the shader must not give R8 an sRGB twin"); Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_UNORM) == VK_FORMAT_R8G8B8A8_SRGB && SrgbTwinFormat(context, VK_FORMAT_BC7_UNORM_BLOCK) == VK_FORMAT_BC7_SRGB_BLOCK, "formats without a shader decode must keep their sRGB twins"); Require(SrgbTwinFormat(context, VK_FORMAT_R32_SFLOAT) == VK_FORMAT_UNDEFINED, "formats without an sRGB counterpart have no twin"); + Require(SrgbTwinFormat(context, VK_FORMAT_R8G8B8A8_SNORM) == VK_FORMAT_R8G8B8A8_SRGB, "8_8_8_8 SNORM must read its bytes through the sRGB twin"); } template diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp index 331882d14a..0178717e94 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleForceDegamma.cpp @@ -27,6 +27,7 @@ constexpr std::uint32_t Format8888Srgb = 130; constexpr std::uint32_t Format32Float = 22; constexpr std::uint32_t Format8888SNorm = 57; constexpr std::uint32_t FormatBc4UNorm = 175; +constexpr std::uint32_t Format32UInt = 20; constexpr std::uint32_t Type2D = 9; constexpr std::uint32_t FilterPoint = 0; constexpr std::uint32_t FilterBilinear = 1; @@ -177,7 +178,11 @@ int main() { RequireSame(Sample(*device, Format8888UNorm, FilterBilinear, ForceDegamma), forcedBilinear, "8_8_8_8_UNORM, bilinear", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); RequireSame(Sample(*device, Format32Float, FilterPoint, ForceDegamma), Sample(*device, Format32Float, FilterPoint, 0u), "32_FLOAT, point"); RequireSame(Sample(*device, FormatBc4UNorm, FilterPoint, ForceDegamma), Sample(*device, FormatBc4UNorm, FilterPoint, 0u), "BC4_UNORM, point"); - Reject(*device, Format8888SNorm, "guest format 57, which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding"); + FillInput(true); + RequireSame(Sample(*device, Format8888SNorm, FilterPoint, ForceDegamma), forcedPoint, "8_8_8_8_SNORM, point", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + FillInput(false); + RequireSame(Sample(*device, Format8888SNorm, FilterBilinear, ForceDegamma), forcedBilinear, "8_8_8_8_SNORM, bilinear", "through FORCE_DEGAMMA", "as 8_8_8_8_SRGB"); + Reject(*device, Format32UInt, "guest format 20, which is not sRGB and has no sRGB view, is sampled through a sampler that forces sRGB decoding"); std::puts("image sample force degamma tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 0c81e0f71c..43c2973971 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -169,7 +169,7 @@ - [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. - [Min/max S# reduction](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. - [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Measured on RDNA2 (gfx1030 in [#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616), gfx1035 in #5821), not on the console: without derivatives (`image_sample_lz`), min or max with anisotropic bilinear returns the bilinear minimum or maximum and with anisotropic point the point result, at every ratio field 0 to 4; with derivatives (`image_sample_d`, a 4:1 footprint), anisotropic bilinear at ratio field 2 or 4 reduces over the texels of all its anisotropic taps (the minimum or maximum along the major axis), not one bilinear footprint. A host that is not AMD reduces over its own anisotropic footprint, and Vulkan leaves nearest filtering with anisotropy to the host driver. -- [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for an SNORM `8`, `8_8` or `8_8_8_8` texture, which RDNA2 (gfx1031) reads bit for bit as the sRGB format of the same bytes, and for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering); `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. +- [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering), and SNORM `8`, `8_8` and `8_8_8_8` through the sRGB view of their bytes, as gfx1031 reads them; `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. - [Tessellation factor ring](../../../core/libs/prx/libSceAgcDriver/State/src/Configuration.cpp) (libSceAgcDriver) - `sceAgcDriverSetTFRing` records the ring and `sceAgcDriverGetTFRing` reports it. Before a title sets one, it reports the console's default, 0xFF0000000 and 128 KiB, as measured in [PS5_Vulkan's hardware findings](https://github.com/mihawk-99/PS5_Vulkan/blob/fde9e3798c2ec1d8fccc90d5067cd6060490607f/docs/HARDWARE_FINDINGS.md). Nothing is written to the recorded ring: tessellated draws run as Vulkan tessellation stages, which keep their factors on the host, so a title that reads tessellation factors from the ring finds none. A null base or a zero size throws; [ps5-opengl's AGC runtime](https://github.com/blackbearreloaded/ps5-opengl/blob/67c873f985eaefe0a72f713529e5632eaa01139c/src/platform/ps5_agc_native_runtime.c) calls `sceAgcDriverSetTFRing(0, 0)` in its cleanup, and what that does on the console is unknown. - [VertexBufferReadSize](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) and the [vertex staging copy](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - a vertex fetch whose index is at or past the descriptor's `num_records` no longer throws for `OOB_SELECT` 0 and 1. The copy still spans the whole fetch, but the bytes from `num_records * stride` onward read as zeros, so the out-of-range vertex is `(0, 0, 0, 0)`. Measured with `buffer_load_format_xyzw` (`idxen`) through the [hardware oracle](../HW_ORACLE.md) on RDNA2 gfx1036 for PPSA01325's word 3 `0x0004dfac` (`OOB_SELECT` 0) in #2241: indices 3, 4 and 5 over 3 records each return `00000000 00000000 00000000 00000000`, W included, unlike Vulkan `robustBufferAccess`, which returns `(0, 0, 0, 1)`. The zeros come from the record check, not from memory: stored values placed past the records are ignored, and a `num_records` 6 control returns them. `RESOURCE_LEVEL` 1, `OOB_SELECT` 1 and FORMAT 74 `32_32_32_FLOAT` give the same zeros, in wave32 and wave64; gfx1013 (the console) is not measured. `OOB_SELECT` 2 returns the stored values past the records and `OOB_SELECT` 3 compares `num_records` against the byte offset, both measured on the same setup in #2241; the two keep throwing past `num_records` here, since each needs its own rule. Zero-stride descriptors (`stride` 0) keep their separate byte-count check and are not covered here. Each out-of-range fetch is copied alone and outside the draw-snapshot cache, which is neither read nor written for it: a reused snapshot would carry another draw's bytes into the zero region, and a stored zero-padded copy would hand zeros to a later in-range draw at the same address. The valid prefix is still flushed for GPU writes before it is read. - [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. From 57e3e7c645c5e265760a5ebce5d39a366b4c69b8 Mon Sep 17 00:00:00 2001 From: mugurc Date: Tue, 20 Oct 2026 10:29:01 +0300 Subject: [PATCH 016/318] fix(libs): build getifaddrs and two tests on macOS hosts Three recent changes use what only Linux has, so libc and two tests no longer compile on macOS: - The AF_LINK entries that getifaddrs lists on Linux use linux/if_link.h, netpacket/packet.h, sockaddr_ll and rtnl_link_stats, and were built on every host but Windows. They are now built on Linux only: macOS lists the IPv4 and IPv6 addresses as before, as Windows does, and the interface test checks the AF_LINK entries on Linux only. - The NetCtlAp test loops over a braced list without including , which libc++ does not pull in through the test's other headers. - The import block test backs its 16 KiB views with memfd_create. The views case is now built on Linux only; macOS runs the case without views, as Windows does. --- .../prx/libSceAgcDriver/tests/BdaResources.cpp | 4 ++-- core/libs/prx/libc/src/Interfaces.cpp | 12 ++++++++++-- core/libs/tests/GuestInterfaces.cpp | 14 +++++++++----- core/libs/tests/GuestNetCtlAp.cpp | 1 + docs/dev/debt/libc.md | 2 +- 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 6d625cf165..1a2e40f45a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -1103,7 +1103,7 @@ public: static constexpr std::size_t Window = std::size_t{8} << 20u; explicit PieceWindow(const Context& context, bool views = false) : importing(context) { -#ifndef _WIN32 +#ifdef __linux__ if (views) { reserved = mmap(nullptr, 2 * Window, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); Require(reserved != MAP_FAILED, "cannot reserve the block of 16 KiB views"); @@ -3369,7 +3369,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importRequesterTests(context, access); hostImportReportTests(); importBlockMapTests(context, false); -#ifndef _WIN32 +#ifdef __linux__ importBlockMapTests(context, true); #endif importBlockBuildTests(context); diff --git a/core/libs/prx/libc/src/Interfaces.cpp b/core/libs/prx/libc/src/Interfaces.cpp index 8e39cfa29b..a5be240839 100644 --- a/core/libs/prx/libc/src/Interfaces.cpp +++ b/core/libs/prx/libc/src/Interfaces.cpp @@ -5,11 +5,13 @@ #include #else #include -#include #include -#include #include +#ifdef __linux__ +#include +#include #include +#endif #include #include #include @@ -160,6 +162,7 @@ int HostEntries(std::vector& entries) { return 0; } #else +#ifdef __linux__ constexpr std::uint8_t guestLinkFamily = 18; constexpr std::size_t guestLinkNameArea = 8 + 16; constexpr std::size_t guestLinkMinimum = 54; @@ -171,6 +174,7 @@ constexpr std::uint8_t guestLinkStateDown = 1; constexpr std::uint8_t guestLinkStateUp = 2; constexpr std::size_t guestInterfaceDataSize = 152; constexpr unsigned int hostLowerUp = 0x10000; +#endif struct HostInterface { std::string name; @@ -179,6 +183,7 @@ struct HostInterface { std::vector addresses; }; +#ifdef __linux__ template void Store(std::vector& bytes, std::size_t offset, TValue value) { std::memcpy(bytes.data() + offset, &value, sizeof(value)); @@ -253,6 +258,7 @@ std::vector GuestInterfaceData(const std::string& name, unsigned i } return data; } +#endif std::uint32_t InterfaceFlags(unsigned int host) { constexpr std::pair flags[] = { @@ -288,6 +294,7 @@ int HostEntries(std::vector& entries) { try { for (auto* item = host; item; item = item->ifa_next) { if (!item->ifa_addr) continue; +#ifdef __linux__ if (item->ifa_addr->sa_family == AF_PACKET) { const auto& link = *reinterpret_cast(item->ifa_addr); HostInterface& owner = interfaceFor(CheckedName(item->ifa_name)); @@ -302,6 +309,7 @@ int HostEntries(std::vector& entries) { owner.linked = true; continue; } +#endif if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; if (!item->ifa_netmask) throw std::runtime_error(std::string("getifaddrs: host address without a netmask on ") + item->ifa_name); Entry entry{CheckedName(item->ifa_name), InterfaceFlags(item->ifa_flags), GuestAddress(item->ifa_addr), GuestAddress(item->ifa_netmask), {}}; diff --git a/core/libs/tests/GuestInterfaces.cpp b/core/libs/tests/GuestInterfaces.cpp index 5b69f12016..c737ea2e11 100644 --- a/core/libs/tests/GuestInterfaces.cpp +++ b/core/libs/tests/GuestInterfaces.cpp @@ -7,9 +7,11 @@ #include #include #include -#include #include +#ifdef __linux__ +#include #include +#endif #include #include #include @@ -76,7 +78,7 @@ static HostInterfaceAddress HostAddress(const sockaddr* address) { return entry; } -#ifndef _WIN32 +#ifdef __linux__ static std::uint64_t SysfsCounter(const char* name, const char* file) { std::ifstream input(std::string("/sys/class/net/") + name + "/statistics/" + file); unsigned long long value = 0; @@ -151,6 +153,7 @@ static std::vector HostSnapshot() { try { for (auto* item = host; item; item = item->ifa_next) { if (!item->ifa_addr) continue; +#ifdef __linux__ if (item->ifa_addr->sa_family == AF_PACKET) { const auto& link = *reinterpret_cast(item->ifa_addr); HostInterfaceAddress entry{}; @@ -165,6 +168,7 @@ static std::vector HostSnapshot() { members.insert(members.begin(), entry); continue; } +#endif if (item->ifa_addr->sa_family != AF_INET && item->ifa_addr->sa_family != AF_INET6) continue; Require(item->ifa_netmask != nullptr); HostInterfaceAddress entry = HostAddress(item->ifa_addr); @@ -192,7 +196,7 @@ int main() { const std::uint8_t loopbackAddress[] = {127, 0, 0, 1}; const std::uint8_t loopbackMask[] = {255, 0, 0, 0}; bool matched = false; -#ifndef _WIN32 +#ifdef __linux__ bool linkChecked = false; SendLoopbackDatagram(); #endif @@ -211,7 +215,7 @@ int main() { Require((item->flags & ~knownFlags) == 0); const auto* address = static_cast(item->address); Require(address != nullptr && reinterpret_cast(address) % 8 == 0); -#ifndef _WIN32 +#ifdef __linux__ if (address[1] == 18) { Require(item->netmask == nullptr && item->destination == nullptr); const auto* data = static_cast(item->data); @@ -295,7 +299,7 @@ int main() { freeifaddrs_nid_postfix(list); } Require(matched); -#ifndef _WIN32 +#ifdef __linux__ Require(linkChecked); #endif GuestInterfaceAddress* released = nullptr; diff --git a/core/libs/tests/GuestNetCtlAp.cpp b/core/libs/tests/GuestNetCtlAp.cpp index fbb8b0f01d..b434dbf9c9 100644 --- a/core/libs/tests/GuestNetCtlAp.cpp +++ b/core/libs/tests/GuestNetCtlAp.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include extern "C" { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e06d2e9b22..696b16ddbe 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -96,7 +96,7 @@ - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. -- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows omits per-interface AF_LINK entries and interfaces without an address; its flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). +- [getifaddrs](../../../core/libs/prx/libc/src/Interfaces.cpp) (libc) lists host interfaces and IPv4/IPv6 addresses in FreeBSD's layouts. Windows and macOS omit per-interface AF_LINK entries and interfaces without an address; on Windows the flags are only `IFF_UP`, `IFF_RUNNING`, `IFF_LOOPBACK`, `IFF_BROADCAST` and `IFF_MULTICAST` (PPP/tunnel adapters omit broadcast and point-to-point, and an unspecified prefix length gets a host mask). On Linux, `AF_LINK` `if_data` maps non-Ethernet/loopback/PPP types to `IFT_OTHER` (1) and leaves `ifi_omcasts`, `ifi_noproto`, `ifi_hwassist`, `ifi_metric`, `ifi_epoch`, `ifi_lastchange`, unknown baud rates and address-entry `ifa_data` as 0/null; a point-to-point address without a peer loses `IFF_POINTOPOINT`, and an omitted IPv4 broadcast is derived from the netmask. Interface names are the host's (`if` on Windows above 15 bytes). - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports the fixed guest name `AnyPS5` without querying the host machine name. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); the console's configurable hostname and its integration with `sysctl` and name resolution are unverified and not implemented. - [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x7D480 to 0x7D920), which converts to and from UTF-8 whatever the locale, with the original sequences of up to six bytes and no check for surrogates or for values above 0x10FFFF when encoding. The conversion state keeps the value in its first eight bytes and the count of bytes still expected (or 0x10 for a pending low surrogate, 1 for a stored high surrogate) as 16 bits at offset 0xA; a null state, which the console dereferences, throws. As on the console, `mbrtoc16` does not store the complete code point when it returns a high surrogate, so the low surrogate of the next call is built from the value the state last held, which is the partial value of an earlier incomplete call or whatever the caller initialised it to, `c16rtomb` returns -1 without setting `errno` when a high surrogate is not followed by a low one, and `mbrtoc16` and `mbrtoc32` report `EILSEQ` as 86. Measured on a PS5 (firmware 13.60) from a homebrew payload, the system `libSceLibcInternal`'s default locale also converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen` (`wctomb` of U+263A gives `E2 98 BA`, `mblen` of a lone 0xE9 gives -1); this libc keeps its single-byte C locale, so [wctomb and mblen](../../../core/libs/prx/libc/src/Multibyte.cpp), which call `wcrtomb` and `mbtowc` here as they call the same internal routines on the console, share that difference. Titles bundle their own `libc.prx`, whose locale was not measured From 381c3b974454e5015547107c8316b3a4820e60e1 Mon Sep 17 00:00:00 2001 From: mugurc Date: Tue, 20 Oct 2026 11:25:39 +0300 Subject: [PATCH 017/318] fix(tests): skip the 1023-byte guest path check on macOS hosts macOS fails a host path of 1024 bytes or more with ENAMETOOLONG, and the host form of a guest path is the root directory followed by the guest path, so a guest path of 1023 bytes, which FreeBSD accepts, fails there under any root directory. guest_path_name_length skips that check on macOS, and a debt entry for guest path names gives the macOS limit. --- core/libs/tests/GuestPathNameLength.cpp | 2 ++ docs/dev/debt/libc.md | 1 + 2 files changed, 3 insertions(+) diff --git a/core/libs/tests/GuestPathNameLength.cpp b/core/libs/tests/GuestPathNameLength.cpp index e07722661b..793fd3e651 100644 --- a/core/libs/tests/GuestPathNameLength.cpp +++ b/core/libs/tests/GuestPathNameLength.cpp @@ -55,7 +55,9 @@ int main() { Require(TooLong(below + longName + "/"), "a long component followed by a trailing slash must fail with ENAMETOOLONG (63)"); Require(!TooLong(below + std::string(200, 'n') + "\\" + std::string(100, 'n')), "a backslash separates components here, so 200 + 100 bytes around it are accepted (FreeBSD would refuse the 301-byte name)"); const std::string block = name255 + "/"; +#ifndef __APPLE__ Require(!TooLong(block + block + block + name255), "a path of exactly 1023 bytes must not be too long"); +#endif Require(TooLong(block + block + block + block), "a path of exactly 1024 bytes must fail with ENAMETOOLONG (63)"); Require(rmdir_nid_postfix("anyps5_name_length") == 0, "rmdir of the test directory failed"); Require(Fails(stat_nid_postfix((longName + "/x").c_str(), status), ErrorNameTooLong), "stat of a long first component must fail with ENAMETOOLONG (63)"); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e06d2e9b22..a7a0ee5f29 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -64,6 +64,7 @@ - [fgetws, getwc, getwchar, putwc, putwchar](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - wide stream I/O encodes and decodes wide characters as UTF-8 on the underlying byte stream; `fgetws` reads characters into the destination buffer until newline or EOF and null-terminates, returning null on stream error. Console wide stream buffering and encoding details remain unverified. - [Closed standard descriptors](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - on Linux and macOS, a descriptor 0, 1 or 2 that is closed when libc loads is opened on `/dev/null` (read-only for 0, write-only for 1 and 2) by a static initializer of libc, which runs before the libraries that depend on libc initialize and before the title starts. Within libc this relies on no static initializer opening a descriptor before it, which `guest_closed_standard_descriptors` checks. glibc does the same for set-user-ID programs ([`check_fds.c`](https://sourceware.org/git/?p=glibc.git;a=blob;f=csu/check_fds.c)). Without it the first descriptor a library opens, the write watch's `userfaultfd` for one, becomes the guest's standard input or output. What the console gives a title as its standard descriptors is not measured, and on Windows a process started without standard handles is not handled. - [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) are checked against FreeBSD 11.4's `NAME_MAX` (255 bytes per component) and `MAXPATHLEN` (1024 bytes with the terminating NUL) before they are resolved, and a longer name fails with `ENAMETOOLONG`, as `namei` does. The host's own limit still applies below that: on Windows a path whose host form exceeds `MAX_PATH` fails with `ENOENT`. A backslash counts as a path separator for the component length, because the guest path resolver turns it into `/` before the host call, while FreeBSD treats it as an ordinary name character, so a 301-byte name that contains one is accepted here. `sceNgs2ParseWaveformFile` resolves its path without a try block, so a long component ends in an uncaught `filesystem_error` there where a missing file ended in a `runtime_error` (both end the title). A path through a regular file (`file/x`, `file/`) is not always `ENOTDIR` on the calls that resolve it. None of this was measured on a console. +- [Guest path names](../../../core/libs/prx/libc/src/General.cpp) (libc) fail on macOS with `ENAMETOOLONG` once their host form, the root directory followed by the guest path, reaches 1024 bytes, so a guest path of 1023 bytes, which FreeBSD accepts, fails there under any root directory - [ftrylockfile](../../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it - [fgetln](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libSceLibcInternal, implemented in libc) throws: in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x5FE80) the export is a `hlt` instruction, which faults in user mode and ends the process - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported From ee6efa9fce2dad24a33a2ac4bc9f3396787e6bc2 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 20 Oct 2026 08:28:19 +0000 Subject: [PATCH 018/318] fix(agc): leave a multisampled DCC target's CMASK unchecked without fast clears The initial import of a multisampled DCC target required its CMASK to be uniformly 0xcc or the fully expanded code. When CB_COLOR_INFO enables COMPRESSION but not FAST_CLEAR, the CMASK only holds FMASK compression, which picks the fragment each sample reads, and a uniform DCC clear sets every fragment to the clear color. The CMASK is then not checked, and the target imports as the DCC clear. METAL GEAR SOLID 4's launcher (PPSA18194) opens its Master Book on a 2-sample 1920x1080 target whose DCC reads as the 0000 clear and whose CMASK holds stale bytes, with FAST_CLEAR off. It stopped there. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/MultisampleTarget.cpp | 2 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 18 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 01f22449f5..2c91e24553 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -727,7 +727,7 @@ VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span(cmask.front()); const auto expanded = target.samples == 2 ? 0xddu : target.samples == 4 ? 0xeeu : 0xffu; Require(std::ranges::all_of(cmask, [&](std::byte key) { return key == cmask.front(); }) && (code == 0xccu || code == expanded), "multisampled DCC CMASK compression or register clear is not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 67d5b61cbe..e5ce7aaea6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -3747,6 +3747,24 @@ void multisampleDccStateTests() { changed.context[0x3b0] |= 1u << 28u; expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "multisampled color mips"); } + for (const auto samples : {2u, 4u, 8u}) { + AgcDriver::Graphics::ColorTarget target{}; + target.extent = {64, 64}; + target.format = VK_FORMAT_R8G8B8A8_UNORM; + target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + target.samples = samples; + target.dccAddress = 0x200000u; + target.dccPipeAligned = true; + target.cmaskAddress = 0x300000u; + target.cmaskBytes = AgcDriver::Graphics::CmaskBytes(64, 64); + const std::vector dcc(AgcDriver::Graphics::MultisampledDccBytes(target), std::byte{0x00}); + std::vector cmask(target.cmaskBytes); + for (std::size_t index = 0; index < cmask.size(); ++index) cmask[index] = std::byte{static_cast(index * 37u)}; + const auto clear = AgcDriver::Graphics::MultisampledDccClear(target, dcc, cmask); + Require(clear.float32[0] == 0.0f && clear.float32[1] == 0.0f && clear.float32[2] == 0.0f && clear.float32[3] == 0.0f, "a uniform 0000 DCC clear of a multisampled target without CMASK fast clears did not import as 0000"); + target.cmaskFastClear = true; + expectFailure([&] { static_cast(AgcDriver::Graphics::MultisampledDccClear(target, dcc, cmask)); }, "multisampled DCC CMASK compression or register clear"); + } } void multisampledDepthTests() { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 5ed4ff62f9..2bf0fcdafe 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -142,6 +142,7 @@ - [Array layer views](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (libSceAgcDriver) - `ArrayLayersServeView` lets a 2D texture at a 2D array image's base address, or a 2D array texture over its first layers, view that image (`Texture::CanCopyFrom`). Mesa-built homebrew (ps5-opengl) writes the view's last slice plus one to `CB_COLOR_ATTRIB3.MIP0_DEPTH`, so a slice-0 render target is a two-layer array image, and the title samples it as a plain 2D texture (SuperTuxKart PPSA69421 drew a black 3D scene and minimap without this). The per-layer layout of a mipmapped array is assumed equal to a standalone surface's; it is not measured on the console. A 2D texture at a later slice's address still gets an image of its own. - [Multisampled CMASK](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a multisampled color target's CMASK is decoded under CB_COLOR_INFO FAST_CLEAR (bit 13, as for a single-sample target) or COMPRESSION (bit 14). Which bit a multisampled target sets is inferred: its fast clears were seen on GTA V's multisampled targets through their CMASK fills, not in a register dump. A COMPRESSION-only target's zero CMASK is not read as cleared (inferred: zero means cleared only under FAST_CLEAR), so only a fill after its first use clears it. A slice of a multisampled array is its own image at its own address with its own CB_COLOR0_CMASK (inferred from the register layout). Its CMASK is main's whole-metablock size. - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. +- [Multisampled DCC CMASK without fast clears](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the initial CMASK of a multisampled DCC target is not checked when CB_COLOR_INFO enables COMPRESSION but not FAST_CLEAR: it then holds only FMASK compression, and a uniform DCC clear sets every fragment, so the target imports as that clear. What the console reads where such an FMASK names no fragment, as an uninitialized CMASK can, is unverified. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. From 070e44768c40f7ceaf95d6220549f07dee724a03 Mon Sep 17 00:00:00 2001 From: mugurc Date: Tue, 20 Oct 2026 11:55:07 +0300 Subject: [PATCH 019/318] fix(tests): let the TZ flipper of the zone race yield The zone race of guest_time_conversion changes TZ in a tight loop while six threads convert times. On macOS the flipper keeps the guest environment lock: the default mutex there lets the thread that unlocks take the lock again before a woken waiter runs. The readers mostly waited, and the test ran for 21 seconds, past its 20-second timeout; a run with counters showed about 373 million TZ changes against the readers' 15,000 local time conversions. The flipper now yields after each change. It still changes TZ about 800,000 times while the readers convert, they see each zone in about half of their conversions, and the test takes about a second. --- core/libs/tests/GuestTimeConversion.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index 2330a5acba..50518721ac 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -372,7 +372,10 @@ static void CheckZoneRace() { std::atomic stop{false}; std::atomic torn{0}; std::thread flipper([&] { - for (bool west = false; !stop.load(); west = !west) setenv_nid_postfix("TZ", west ? "UTC-5" : "UTC+3", 1); + for (bool west = false; !stop.load(); west = !west) { + setenv_nid_postfix("TZ", west ? "UTC-5" : "UTC+3", 1); + std::this_thread::yield(); + } }); std::array readers; for (auto& reader : readers) { From 6a4913cc7cc6e185f98f6b0c8792bd5c5c70b476 Mon Sep 17 00:00:00 2001 From: hypermist Date: Tue, 20 Oct 2026 22:32:46 +1300 Subject: [PATCH 020/318] feat(agc): write the workload packets on the ACB and for an inactive stream sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete and sceAgcAcbSetWorkloadStreamInactive build the same packets as the DCB calls, and sceAgcDcbSetWorkloadStreamInactive writes a 9-dword IT_NOP with the stream id, the size of the console's user-data packet. The debt entry now records the console layout read from libSceAgcDriver. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAgc/Acb/src/Control.cpp | 21 ++------ .../libSceAgc/DcbState/include/Workload.hpp | 11 ++++ .../prx/libSceAgc/DcbState/src/Workload.cpp | 50 +++++++++++++------ core/libs/prx/libSceAgc/tests/Commands.cpp | 17 ++++--- docs/dev/debt/libSceAgc.md | 3 +- 5 files changed, 61 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index 567bfd5e18..539066325a 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -4,23 +4,13 @@ #include "prx/libSceAgc/Command/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgc/DcbState/include/Marker.hpp" +#include "prx/libSceAgc/DcbState/include/Workload.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" -#include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" -namespace { - -std::uint32_t* RejectUnregisteredWorkloadStream(CommandBuffer* buf, std::uint32_t streamId, const char* function) { - if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; - NotImplemented_nid_no_patch(function); - return nullptr; -} - -} - extern "C" { std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { @@ -83,18 +73,15 @@ std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t vide } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) { - (void)workloadIds; - (void)workloadCount; - return RejectUnregisteredWorkloadStream(buf, streamId, __func__); + return Agc::Workload::Active(buf, streamId, workloadIds, workloadCount, __func__); } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId) { - (void)workloadId; - return RejectUnregisteredWorkloadStream(buf, streamId, __func__); + return Agc::Workload::Complete(buf, streamId, workloadId, __func__); } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId) { - return RejectUnregisteredWorkloadStream(buf, streamId, __func__); + return Agc::Workload::StreamInactive(buf, streamId, __func__); } uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uint32_t color) { diff --git a/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp b/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp index 3d5d6cac71..75ae9c3979 100644 --- a/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp +++ b/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp @@ -1,4 +1,15 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_WORKLOAD_HPP #define CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_WORKLOAD_HPP +#include +#include "SceTypes.hpp" + +namespace Agc::Workload { + +std::uint32_t* Active(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount, const char* function); +std::uint32_t* Complete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId, const char* function); +std::uint32_t* StreamInactive(CommandBuffer* buf, std::uint32_t streamId, const char* function); + +} + #endif diff --git a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp index 0f739d9055..06d5578a81 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp @@ -13,6 +13,7 @@ namespace { constexpr std::uint32_t OpcodeNop = 0x10; constexpr std::uint32_t ActivePacketWords = 18; constexpr std::uint32_t CompletePacketWords = 12; +constexpr std::uint32_t InactivePacketWords = 9; constexpr std::uint32_t MaxWorkloadId = 63; constexpr std::uint32_t MaxActiveCount = 63; @@ -25,40 +26,57 @@ uint32_t* NewPacket(CommandBuffer* buf, std::uint32_t words, const char* functio } -extern "C" { +namespace Agc::Workload { -uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { - if (buf == nullptr || workload_id > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; - const std::uint64_t clearMask = ~(std::uint64_t{1} << workload_id); - auto* packet = NewPacket(buf, CompletePacketWords, __func__); - packet[1] = stream_id; - packet[2] = workload_id; +std::uint32_t* Complete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId, const char* function) { + if (buf == nullptr || workloadId > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + const std::uint64_t clearMask = ~(std::uint64_t{1} << workloadId); + auto* packet = NewPacket(buf, CompletePacketWords, function); + packet[1] = streamId; + packet[2] = workloadId; packet[3] = static_cast(clearMask); packet[4] = static_cast(clearMask >> 32); return packet; } -uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { - if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; +std::uint32_t* Active(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount, const char* function) { + if (buf == nullptr || workloadIds == nullptr || workloadCount == 0 || workloadCount > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; std::uint64_t mask = 0; - for (std::uint32_t index = 0; index < workload_count; ++index) { - const auto id = workload_ids[index]; + for (std::uint32_t index = 0; index < workloadCount; ++index) { + const auto id = workloadIds[index]; if (id > MaxWorkloadId) return nullptr; const std::uint64_t bit = std::uint64_t{1} << id; if ((mask & bit) != 0) return nullptr; mask |= bit; } - auto* packet = NewPacket(buf, ActivePacketWords, __func__); - packet[1] = stream_id; + auto* packet = NewPacket(buf, ActivePacketWords, function); + packet[1] = streamId; packet[2] = static_cast(mask); packet[3] = static_cast(mask >> 32); return packet; } -uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) { +std::uint32_t* StreamInactive(CommandBuffer* buf, std::uint32_t streamId, const char* function) { if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; - NotImplemented_nid_no_patch(__func__); - return nullptr; + auto* packet = NewPacket(buf, InactivePacketWords, function); + packet[1] = streamId; + return packet; +} + +} + +extern "C" { + +uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { + return Agc::Workload::Complete(buf, stream_id, workload_id, __func__); +} + +uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { + return Agc::Workload::Active(buf, stream_id, workload_ids, workload_count, __func__); +} + +uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) { + return Agc::Workload::StreamInactive(buf, streamId, __func__); } } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 336c49b51f..71612289ab 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -294,12 +294,17 @@ void testWorkloadStreams() { check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); Storage registered; registered.words.fill(0xdeadbeefu); - const auto untouched = registered.words; - expectFailure([&] { sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); }); - expectFailure([&] { sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2); }); - expectFailure([&] { sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 3); }); - expectFailure([&] { sceAgcAcbSetWorkloadStreamInactive(®istered.buffer, 1); }); - check(registered.words == untouched && registered.buffer.cursor_up == registered.words.data(), "a workload call that threw modified the buffer"); + const auto* inactive = sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); + check(inactive == registered.words.data() && inactive[0] == Agc::Command::Header(0x10, 9, 0) && inactive[1] == 1, "DCB workload stream inactive packet"); + for (std::uint32_t word = 2; word < 9; ++word) check(inactive[word] == 0, "DCB workload stream inactive packet tail is not zero"); + check(registered.buffer.cursor_up == registered.words.data() + 9, "DCB workload stream inactive cursor advance"); + Storage dcb; + Storage acb; + check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2) != nullptr && sceAgcAcbSetWorkloadsActive(&acb.buffer, 1, ids, 2) == acb.words.data(), "ACB workloads active packet"); + check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) != nullptr && sceAgcAcbSetWorkloadComplete(&acb.buffer, 1, 3) == acb.words.data() + 18, "ACB workload complete packet"); + check(sceAgcDcbSetWorkloadStreamInactive(&dcb.buffer, 1) != nullptr && sceAgcAcbSetWorkloadStreamInactive(&acb.buffer, 1) == acb.words.data() + 30, "ACB workload stream inactive packet"); + check(acb.buffer.cursor_up == acb.words.data() + 39 && dcb.words == acb.words, "ACB workload packets differ from the DCB ones"); + check(sceAgcAcbSetWorkloadsActive(&acb.buffer, 1, ids, 0) == nullptr && sceAgcAcbSetWorkloadComplete(&acb.buffer, 1, 64) == nullptr, "ACB workload argument checks"); check(sceAgcDcbSetWorkloadStreamInactive(nullptr, 1) == nullptr && sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr && sceAgcAcbSetWorkloadComplete(nullptr, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(nullptr, 1) == nullptr, "a workload call with a null command buffer for a registered stream returned a packet"); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 53522e9736..d9c6839824 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -42,7 +42,6 @@ - [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents - [sceAgcSetAmmSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the `(memory, sizeInBytes)` and `(index, label)` signatures, the 16 KiB alignment of the memory and its size, the single registration, the 32-byte labels and the zeroed first 8 bytes of each are what [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L508-L546) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Agc/AgcExports.AmmSemaphore.cs) agree on; KytyPS5 has neither function. OpenAGC clears the whole memory and sharpemu only those 8 bytes, so a registration whose labels hold data past them throws. The two return different errors for every rejected call (OpenAGC `0x8a6c0002`, `0x8a6c000b`, `0x8a6c0048` and `0x8a6c0049`, sharpemu generic invalid-argument and already-exists errors), so every rejected call throws, including a lookup before registration. Not measured on the console; PPSA02801 imports both (OpenAGC's import analysis), no title is shown calling them. -- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. -- [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting +- [sceAgcDcbSetWorkloadsActive, sceAgcDcbSetWorkloadComplete, sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp), [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the console packets are not written. The firmware builds them with libSceAgcDriver's sceAgcDriverSetWorkloadsActive, sceAgcDriverSetWorkloadComplete and sceAgcDriverSetWorkloadStreamInactive: a SET_UCONFIG_REG of a tag to 0x342 (`0xCC000000 | stream` for active and inactive, `0xCD000000 + workload * 32 + stream` for complete) that carries the first dword of an 8-byte payload (the active mask, zero for inactive), a WRITE_DATA of the rest to register 0xC343, and for active and complete a 9-dword ATOMIC_MEM that ORs the mask into, or ANDs the cleared bit out of, the stream's 64-bit word in a table the driver takes at module start (control 0x267 or 0x275, bit 30 set on the DCB). The executor has no ATOMIC_MEM, the table is not allocated, and an unpredicated SET_UCONFIG_REG is rejected on a compute queue, so the calls write an `IT_NOP` of the console size instead: 18 dwords for active (stream id, mask), 12 for complete (stream id, workload id, clear mask) and 9 for inactive (stream id); the ACB calls write the same packets as the DCB calls. The argument checks are the firmware's: null for a null command buffer, a stream outside 1 to 31 or not registered, a workload id above 63, and for active a null id list, a duplicate id or a count of 0 or above 63; the stream is checked before the ids are read. The driver registration records only the stream id; the console also keeps a 32-byte name per stream and pre-registers stream 0 as "System", and its error codes for an invalid or repeated registration are not returned, so registration never fails here. - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From 6e578483c88a953d77c2a231cdc26081ed6085bf Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 13:46:33 +0400 Subject: [PATCH 021/318] fix(relinker): accept a Windows base relocation whose eight bytes span into the next page --- .../elfpatcher/src/windows/WindowsRelocationBuilder.cpp | 2 -- core/relinker/relinker/tests/ConversionTests.cpp | 6 +++--- core/relinker/relinker/tests/test_none_relocations.py | 7 +++---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index 71b149d37f..7bd368ac26 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -86,8 +86,6 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom std::vector WindowsRelocationBuilder::BuildBaseRelocations(const std::vector& targets) const { std::map> pages; for (const auto target : targets) { - if ((target & 0xfffu) > 0x1000u - 8) - throw Domain::RelinkerException("Base relocation target crosses a page boundary", target); auto& entries = pages[target & ~0xfffu]; const auto entry = static_cast(0xa000u | (target & 0xfffu)); if (std::find(entries.begin(), entries.end(), entry) == entries.end()) diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 0b6d9d62c9..2c9f696d35 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -241,9 +241,9 @@ void baseRelocations() { require(Io::ReadU32(relocs, 4) == 12, "Wrong base relocation block size"); require(Io::ReadU16(relocs, 8) == 0xa000, "Wrong first relocation entry"); require(Io::ReadU16(relocs, 10) == 0xa008, "Wrong second relocation entry"); - reject([&] { (void)builder.BuildBaseRelocations({0x10ffc}); }); - reject([&] { (void)builder.BuildBaseRelocations({0x10ffd}); }); - reject([&] { (void)builder.BuildBaseRelocations({0x10fff}); }); + const auto spanning = builder.BuildBaseRelocations({0x10ffc, 0x10fff}); + require(spanning.size() == 12 && Io::ReadU32(spanning, 0) == 0x10000, "A page-spanning base relocation was not placed in its first page"); + require(Io::ReadU16(spanning, 8) == 0xaffc && Io::ReadU16(spanning, 10) == 0xafff, "Wrong page-spanning base relocation entries"); } } diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 538b0247d8..b117b9781a 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -122,14 +122,13 @@ def main(): page_image = image_with_relocations([(0xffc, 8, 0x210)], False, False) page_image.extend(bytes(0x1000)) struct.pack_into(" Date: Tue, 20 Oct 2026 13:46:34 +0400 Subject: [PATCH 022/318] test(relinker): run a Windows base relocation that spans two pages --- core/relinker/relinker/tests/test_none_relocations.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index b117b9781a..71aab2c87c 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -120,6 +120,7 @@ def main(): assert overflow_result.returncode == 2 and "Relocation target address overflow" in overflow_result.stderr, overflow_result.stderr assert not overflow_output.exists() page_image = image_with_relocations([(0xffc, 8, 0x210)], False, False) + page_image[0x200:0x206] = b"\xff\x25" + struct.pack(" Date: Tue, 20 Oct 2026 13:46:34 +0400 Subject: [PATCH 023/318] test(relinker): check the page-spanning relocation after Windows moves the image --- core/relinker/relinker/tests/test_none_relocations.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 71aab2c87c..40de19163c 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -133,6 +133,15 @@ def main(): if os.name == "nt": run = subprocess.run([str(page_output)], capture_output=True, timeout=20) assert run.returncode == 42, ("page-crossing relocation", run.returncode, run.stderr) + moved = bytearray(page_output.read_bytes()) + optional_header = struct.unpack_from(" Date: Tue, 20 Oct 2026 13:46:35 +0400 Subject: [PATCH 024/318] test(relinker): refuse a base relocation whose eight bytes run past the image --- core/relinker/relinker/tests/test_none_relocations.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/relinker/relinker/tests/test_none_relocations.py b/core/relinker/relinker/tests/test_none_relocations.py index 40de19163c..3c02647db1 100644 --- a/core/relinker/relinker/tests/test_none_relocations.py +++ b/core/relinker/relinker/tests/test_none_relocations.py @@ -142,6 +142,17 @@ def main(): for _ in range(3): run = subprocess.run([str(moved_output)], capture_output=True, timeout=20) assert run.returncode == 42, ("page-crossing relocation after the loader moved the image", run.returncode, run.stderr) + past_end_image = image_with_relocations([(0x1ffc, 8, 0x210)], False, False) + past_end_image.extend(bytes(0x1000)) + struct.pack_into(" Date: Sun, 18 Oct 2026 20:56:18 +0400 Subject: [PATCH 025/318] feat(agc): emulate barycentrics in rect-list draws --- .../Execution/src/Driver/Draw/Draw.cpp | 4 +- .../execution/PixelBarycentricEmulation.cpp | 102 +++++++++++------- .../SpirvBackend/src/RectListShaders.cpp | 95 ++++++++++++++-- 3 files changed, 153 insertions(+), 48 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 1dcbe329eb..42f817a16c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -486,8 +486,8 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe require(programResults[0]->pushConstants.size() == pushBytes && programResults[0]->parameterExports == exports, "pairing the mesh program with its pixel shader changed the push constants or the parameter exports the pixel shader was built for"); } require(programResults[0]->meshBarycentrics == meshBarycentrics, "the mesh program was compiled for a different barycentric layout than its pixel shader"); - } else if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { - require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it"); + } else if (!graphics.rectList && !programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) { + require(graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it"); const auto geometryLimits = localDevice->GeometryLimits(); if (geometryLimits || !localDevice->Target().tessellation) { results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp index 9978674554..0fb12a6cd3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelBarycentricEmulation.cpp @@ -45,11 +45,23 @@ struct Vertex { float w; }; -constexpr std::array Corners{{{-0.8f, -0.8f, 1.0f}, {0.9f, -0.6f, 2.5f}, {-0.5f, 0.85f, 0.5f}}}; +constexpr std::array TriangleCorners{{{-0.8f, -0.8f, 1.0f}, {0.9f, -0.6f, 2.5f}, {-0.5f, 0.85f, 0.5f}}}; +constexpr std::array RectangleCorners{{{-0.8f, -0.8f, 1.5f}, {0.7f, -0.8f, 1.5f}, {-0.8f, 0.6f, 1.5f}}}; -std::array, 3> ClipPositions() { +enum class Emulation { + Geometry, + Tessellation, + RectList, +}; + +const std::array& Corners(Emulation emulation) { + return emulation == Emulation::RectList ? RectangleCorners : TriangleCorners; +} + +std::array, 3> ClipPositions(Emulation emulation) { + const auto& corners = Corners(emulation); std::array, 3> positions{}; - for (std::size_t i = 0; i < Corners.size(); ++i) positions[i] = {Corners[i].x * Corners[i].w, Corners[i].y * Corners[i].w, 0.5f * Corners[i].w, Corners[i].w}; + for (std::size_t i = 0; i < corners.size(); ++i) positions[i] = {corners[i].x * corners[i].w, corners[i].y * corners[i].w, 0.5f * corners[i].w, corners[i].w}; return positions; } @@ -58,16 +70,13 @@ std::array BufferDescriptor(const void* data, std::uint32_t st return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; } -enum class Emulation { - Geometry, - Tessellation, -}; - ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulation emulation, std::span pixelCode, std::uint32_t inputs, VkCullModeFlags cullMode = VK_CULL_MODE_NONE) { Pixels.fill(Unwritten); auto target = device.Target(); target.fragmentShaderBarycentricEnabled = false; - static const auto positions = ClipPositions(); + static const auto trianglePositions = ClipPositions(Emulation::Geometry); + static const auto rectanglePositions = ClipPositions(Emulation::RectList); + const auto& positions = emulation == Emulation::RectList ? rectanglePositions : trianglePositions; std::vector vertexUserData(4, 0u); const auto vertexBuffer = BufferDescriptor(positions.data(), 16u, static_cast(positions.size())); @@ -104,7 +113,12 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulatio std::vector shaders{{ShaderStage::Vertex, &vertexResult, 0}}; ShaderRecompiler::RecompileResult geometry; ShaderRecompiler::BarycentricTessellationShaders patches; - if (pixelResult.barycentricEmulation.active && emulation == Emulation::Geometry) { + ShaderRecompiler::RectListShaders rectangle; + if (emulation == Emulation::RectList) { + rectangle = ShaderRecompiler::BuildRectListShaders(vertexResult, pixelResult, target); + shaders.push_back({ShaderStage::TessellationControl, &rectangle.control, 0}); + shaders.push_back({ShaderStage::TessellationEvaluation, &rectangle.evaluation, 0}); + } else if (pixelResult.barycentricEmulation.active && emulation == Emulation::Geometry) { geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits()); shaders.push_back({ShaderStage::Geometry, &geometry, 0}); } else if (pixelResult.barycentricEmulation.active) { @@ -120,7 +134,8 @@ ShaderRecompiler::RecompileResult Draw(AgcDriver::VulkanDevice& device, Emulatio state.colors = {state.color}; state.hasColorTarget = true; state.renderExtent = {Width, Height}; - state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.topology = emulation == Emulation::RectList ? VK_PRIMITIVE_TOPOLOGY_PATCH_LIST : VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.rectList = emulation == Emulation::RectList; state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; state.negativeOneToOne = false; state.scissor = {{0, 0}, {Width, Height}}; @@ -141,9 +156,10 @@ struct Expected { std::array perspective; }; -Expected Barycentrics(std::uint32_t x, std::uint32_t y, bool snapped) { +Expected Barycentrics(Emulation emulation, std::uint32_t x, std::uint32_t y, bool snapped) { + const auto& corners = Corners(emulation); std::array, 3> window{}; - for (std::size_t i = 0; i < Corners.size(); ++i) window[i] = {(Corners[i].x + 1.0) * Width / 2.0, (1.0 - Corners[i].y) * Height / 2.0}; + for (std::size_t i = 0; i < corners.size(); ++i) window[i] = {(corners[i].x + 1.0) * Width / 2.0, (1.0 - corners[i].y) * Height / 2.0}; if (snapped) { for (auto& corner : window) { for (auto& coordinate : corner) coordinate = std::round(coordinate * 256.0) / 256.0; @@ -154,22 +170,23 @@ Expected Barycentrics(std::uint32_t x, std::uint32_t y, bool snapped) { const auto edge = [](const std::array& a, const std::array& b, double cx, double cy) { return (b[0] - a[0]) * (cy - a[1]) - (b[1] - a[1]) * (cx - a[0]); }; const double area = edge(window[0], window[1], window[2][0], window[2][1]); const std::array b{edge(window[1], window[2], px, py) / area, edge(window[2], window[0], px, py) / area, edge(window[0], window[1], px, py) / area}; - Expected expected{std::min({b[0], b[1], b[2]}) > 0.02, {}, {}}; + const bool inside = emulation == Emulation::RectList ? std::min(b[1], b[2]) > 0.02 && std::max(b[1], b[2]) < 0.98 : std::min({b[0], b[1], b[2]}) > 0.02; + Expected expected{inside, {}, {}}; double sum = 0.0; - for (std::size_t i = 0; i < 3; ++i) sum += b[i] / Corners[i].w; + for (std::size_t i = 0; i < 3; ++i) sum += b[i] / corners[i].w; for (std::size_t i = 0; i < 3; ++i) { expected.linear[i] = static_cast(b[i]); - expected.perspective[i] = static_cast(b[i] / Corners[i].w / sum); + expected.perspective[i] = static_cast(b[i] / corners[i].w / sum); } return expected; } -Expected BarycentricsUnsnapped(std::uint32_t x, std::uint32_t y) { - return Barycentrics(x, y, false); +Expected BarycentricsUnsnapped(Emulation emulation, std::uint32_t x, std::uint32_t y) { + return Barycentrics(emulation, x, y, false); } -Expected BarycentricsSnapped(std::uint32_t x, std::uint32_t y) { - return Barycentrics(x, y, true); +Expected BarycentricsSnapped(Emulation emulation, std::uint32_t x, std::uint32_t y) { + return Barycentrics(emulation, x, y, true); } bool MatchesEither(float value, float unsnapped, float snapped) { @@ -180,13 +197,15 @@ float Pixel(std::uint32_t x, std::uint32_t y, std::uint32_t channel) { return Pixels[(y * Width + x) * 4u + channel]; } -std::uint32_t CheckBarycentrics(const std::string& what, bool perspective) { +std::uint32_t CheckBarycentrics(Emulation emulation, const std::string& what, bool perspective) { std::uint32_t checked = 0; + std::uint32_t beyond = 0; for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { - const auto expected = BarycentricsUnsnapped(x, y); + const auto expected = BarycentricsUnsnapped(emulation, x, y); if (!expected.inside) continue; - const auto snapped = BarycentricsSnapped(x, y); + const auto snapped = BarycentricsSnapped(emulation, x, y); + if (expected.linear[0] < -0.02f) ++beyond; const auto& weights = perspective ? expected.perspective : expected.linear; const auto& snappedWeights = perspective ? snapped.perspective : snapped.linear; for (std::uint32_t channel = 0; channel < 4; ++channel) { @@ -199,17 +218,18 @@ std::uint32_t CheckBarycentrics(const std::string& what, bool perspective) { } } Require(checked > 200u, what + ": too few interior pixels were checked"); + Require(emulation != Emulation::RectList || beyond > 200u, what + ": too few pixels past the rectangle's diagonal were checked"); return checked; } -void CheckVertexValues() { - const auto positions = ClipPositions(); +void CheckVertexValues(Emulation emulation) { + const auto positions = ClipPositions(emulation); const auto p0 = positions[0][0]; const std::array expected{positions[1][0] - p0, positions[2][0] - p0, p0}; std::uint32_t checked = 0; for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { - if (!BarycentricsUnsnapped(x, y).inside) continue; + if (!BarycentricsUnsnapped(emulation, x, y).inside) continue; for (std::uint32_t channel = 0; channel < 3; ++channel) { Require(Pixel(x, y, channel) == expected[channel], "v_interp_mov: pixel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(Pixel(x, y, channel)) + ", expected " + std::to_string(expected[channel])); } @@ -219,14 +239,14 @@ void CheckVertexValues() { Require(checked > 200u, "v_interp_mov: too few interior pixels were checked"); } -void CheckInterpolated() { - const auto positions = ClipPositions(); +void CheckInterpolated(Emulation emulation) { + const auto positions = ClipPositions(emulation); std::uint32_t checked = 0; for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < Width; ++x) { - const auto expected = BarycentricsUnsnapped(x, y); + const auto expected = BarycentricsUnsnapped(emulation, x, y); if (!expected.inside) continue; - const auto snapped = BarycentricsSnapped(x, y); + const auto snapped = BarycentricsSnapped(emulation, x, y); float reference = 0.0f; float snappedReference = 0.0f; for (std::size_t i = 0; i < 3; ++i) { @@ -257,23 +277,23 @@ void CheckWinding(AgcDriver::VulkanDevice& device, Emulation emulation, const st } void RunEmulation(AgcDriver::VulkanDevice& device, Emulation emulation) { - const std::string name = emulation == Emulation::Geometry ? "geometry stage" : "tessellation stages"; + const std::string name = emulation == Emulation::Geometry ? "geometry stage" : emulation == Emulation::Tessellation ? "tessellation stages" : "rect-list stages"; const auto perspective = PixelInputBit(PixelInput::PerspectiveCenter); const auto linear = PixelInputBit(PixelInput::LinearCenter); auto result = Draw(device, emulation, BarycentricPixelCode, perspective); Require(result.barycentricEmulation.active && result.barycentricEmulation.smooth && !result.barycentricEmulation.linear, "reading PERSP_CENTER I/J did not emulate perspective barycentrics"); - const auto smooth = CheckBarycentrics(name + " PERSP_CENTER I/J", true); + const auto smooth = CheckBarycentrics(emulation, name + " PERSP_CENTER I/J", true); result = Draw(device, emulation, BarycentricPixelCode, linear); Require(result.barycentricEmulation.active && !result.barycentricEmulation.smooth && result.barycentricEmulation.linear, "reading LINEAR_CENTER I/J did not emulate linear barycentrics"); - const auto noPerspective = CheckBarycentrics(name + " LINEAR_CENTER I/J", false); + const auto noPerspective = CheckBarycentrics(emulation, name + " LINEAR_CENTER I/J", false); result = Draw(device, emulation, VertexValuePixelCode, perspective); Require(result.barycentricEmulation.active && result.fragmentParameters.size() == 1 && result.fragmentParameters[0].perVertex, "v_interp_mov of P10 and P20 did not emulate per-vertex parameters"); - CheckVertexValues(); + CheckVertexValues(emulation); result = Draw(device, emulation, InterpolatedPixelCode, perspective); Require(!result.barycentricEmulation.active, "plain v_interp_p1/p2 needed a generated stage"); - CheckInterpolated(); - CheckWinding(device, emulation, name); - std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation and face culling\n", name.c_str(), smooth, noPerspective); + CheckInterpolated(emulation); + if (emulation != Emulation::RectList) CheckWinding(device, emulation, name); + std::printf("barycentric emulation through the %s passed: %u perspective and %u linear I/J pixels, raw vertex values, host interpolation%s\n", name.c_str(), smooth, noPerspective, emulation == Emulation::RectList ? "" : " and face culling"); } } @@ -290,8 +310,12 @@ int main() { } if (geometry) RunEmulation(*device, Emulation::Geometry); else std::puts("geometry stage skipped, the device lacks geometryShader"); - if (tessellation) RunEmulation(*device, Emulation::Tessellation); - else std::puts("tessellation stages skipped, the device lacks tessellationShader"); + if (tessellation) { + RunEmulation(*device, Emulation::Tessellation); + RunEmulation(*device, Emulation::RectList); + } else { + std::puts("tessellation and rect-list stages skipped, the device lacks tessellationShader"); + } return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp index bbf6dcc0f6..ed3483d923 100644 --- a/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp +++ b/core/shader/recompiler/SpirvBackend/src/RectListShaders.cpp @@ -26,20 +26,32 @@ struct Parameter { bool missing = false; }; +struct PerVertexSource { + std::uint32_t inputLocation; + std::uint32_t outputLocation; +}; + +struct Emulation { + std::vector barycentrics; + std::vector perVertex; +}; + void require(bool condition, const char* message) { if (!condition) throw std::runtime_error(std::string("Rect-list SPIR-V: ") + message); } class RectListEmitter { public: - RectListEmitter(const std::vector& parameters, spv::ExecutionModel model, std::uint32_t version, std::uint32_t faultBinding, std::uint32_t clipDistances) : builder(version), faultBinding(faultBinding), version(version), clipDistances(clipDistances), parameters(parameters) { + RectListEmitter(const std::vector& parameters, const Emulation& emulation, spv::ExecutionModel model, std::uint32_t version, std::uint32_t faultBinding, std::uint32_t clipDistances) : builder(version), faultBinding(faultBinding), version(version), clipDistances(clipDistances), parameters(parameters), emulation(emulation) { builder.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450); voidType = type(spv::OpTypeVoid); uintType = type(spv::OpTypeInt, 32u, 0u); intType = type(spv::OpTypeInt, 32u, 1u); floatType = type(spv::OpTypeFloat, 32u); + vec3FloatType = type(spv::OpTypeVector, floatType, 3u); vec4FloatType = type(spv::OpTypeVector, floatType, 4u); + vertexArrayType = array(vec4FloatType, 3u); functionType = type(spv::OpTypeFunction, voidType); perVertexType = builder.DecoratedType(spv::OpTypeStruct, {{spv::OpMemberDecorate, {0u, spv::DecorationBuiltIn, spv::BuiltInPosition}}, {spv::OpMemberDecorate, {0u, spv::DecorationInvariant}}, {spv::OpDecorate, {spv::DecorationBlock}}}, vec4FloatType); @@ -51,13 +63,14 @@ public: ptrInputVec4Float = pointer(spv::StorageClassInput, vec4FloatType); ptrOutputVec4Float = pointer(spv::StorageClassOutput, vec4FloatType); + ptrInputVec3Float = pointer(spv::StorageClassInput, vec3FloatType); + ptrOutputVec3Float = pointer(spv::StorageClassOutput, vec3FloatType); if (model == spv::ExecutionModelTessellationControl) { boolType = type(spv::OpTypeBool); vec2BoolType = type(spv::OpTypeVector, boolType, 2u); vec2FloatType = type(spv::OpTypeVector, floatType, 2u); ptrOutputFloat = pointer(spv::StorageClassOutput, floatType); } else { - vec3FloatType = type(spv::OpTypeVector, floatType, 3u); ptrInputFloat = pointer(spv::StorageClassInput, floatType); } } @@ -155,6 +168,22 @@ public: store(access(ptrOutputVec4Float, outputs[i], invocation), value); } + if (!emulation.barycentrics.empty()) { + const auto zero = constant(floatType, 0); + std::array weight {}; + for (std::uint32_t i = 0; i < weight.size(); i++) { + const auto own = result(spv::OpSelect, floatType, result(spv::OpIEqual, boolType, index, intConstant(i)), floatOne, zero); + weight[i] = result(spv::OpSelect, floatType, fourth, barycentric[i], own); + } + const auto weights = result(spv::OpCompositeConstruct, vec3FloatType, weight[0], weight[1], weight[2]); + for (const auto output : barycentricOutputs) store(access(ptrOutputVec3Float, output, invocation), weights); + } + for (std::size_t i = 0; i < emulation.perVertex.size(); i++) { + for (std::uint32_t source = 0; source < 3u; source++) { + store(access(ptrOutputVec4Float, perVertexOutputs[i], invocation, intConstant(source)), load(vec4FloatType, access(ptrInputVec4Float, perVertexInputs[i], intConstant(source)))); + } + } + emit(spv::OpReturn); emit(spv::OpFunctionEnd); return builder.Finalize(); @@ -178,6 +207,8 @@ public: for (std::uint32_t i = 0; i < parameters.size(); i++) { store(outputs[i], load(vec4FloatType, access(ptrInputVec4Float, inputs[i], index))); } + for (std::size_t i = 0; i < barycentricOutputs.size(); i++) store(barycentricOutputs[i], load(vec3FloatType, access(ptrInputVec3Float, barycentricInputs[i], index))); + for (std::size_t i = 0; i < emulation.perVertex.size(); i++) store(perVertexOutputs[i], load(vertexArrayType, access(pointer(spv::StorageClassInput, vertexArrayType), perVertexInputs[i], index))); emit(spv::OpReturn); emit(spv::OpFunctionEnd); @@ -344,6 +375,23 @@ private: decorate(inputs[i], spv::DecorationLocation, location); locations[location] = inputs[i]; } + for (const auto location : emulation.barycentrics) { + if (tessControl) continue; + barycentricInputs.push_back(addInterface(spv::StorageClassInput, array(vec3FloatType, 4u))); + decorate(barycentricInputs.back(), spv::DecorationLocation, location); + } + for (const auto& source : emulation.perVertex) { + if (!tessControl) { + perVertexInputs.push_back(addInterface(spv::StorageClassInput, array(vertexArrayType, 4u))); + decorate(perVertexInputs.back(), spv::DecorationLocation, source.outputLocation); + continue; + } + if (locations[source.inputLocation] == 0) { + locations[source.inputLocation] = addInterface(spv::StorageClassInput, vertexArrayType); + decorate(locations[source.inputLocation], spv::DecorationLocation, source.inputLocation); + } + perVertexInputs.push_back(locations[source.inputLocation]); + } } void defineOutputs(spv::ExecutionModel model) { @@ -367,6 +415,14 @@ private: outputs[i] = addInterface(spv::StorageClassOutput, tessControl ? array(vec4FloatType, 4u) : vec4FloatType); decorate(outputs[i], spv::DecorationLocation, parameters[i].outputLocation); } + for (const auto location : emulation.barycentrics) { + barycentricOutputs.push_back(addInterface(spv::StorageClassOutput, tessControl ? array(vec3FloatType, 4u) : vec3FloatType)); + decorate(barycentricOutputs.back(), spv::DecorationLocation, location); + } + for (const auto& source : emulation.perVertex) { + perVertexOutputs.push_back(addInterface(spv::StorageClassOutput, tessControl ? array(vertexArrayType, 4u) : vertexArrayType)); + decorate(perVertexOutputs.back(), spv::DecorationLocation, source.outputLocation); + } } std::uint32_t interpolate(std::uint32_t v0, std::uint32_t v1, std::uint32_t v2, const std::array& barycentric) { @@ -382,9 +438,14 @@ private: std::uint32_t clipDistances; std::uint32_t fault = 0; const std::vector& parameters; + const Emulation& emulation; std::vector interfaces; std::vector inputs; std::vector outputs; + std::vector barycentricInputs; + std::vector barycentricOutputs; + std::vector perVertexInputs; + std::vector perVertexOutputs; std::uint32_t main = 0; std::uint32_t voidType = 0; std::uint32_t boolType = 0; @@ -395,6 +456,7 @@ private: std::uint32_t vec2FloatType = 0; std::uint32_t vec3FloatType = 0; std::uint32_t vec4FloatType = 0; + std::uint32_t vertexArrayType = 0; std::uint32_t functionType = 0; std::uint32_t perVertexType = 0; std::uint32_t clipArrayType = 0; @@ -404,6 +466,8 @@ private: std::uint32_t ptrInputVec4Float = 0; std::uint32_t ptrOutputFloat = 0; std::uint32_t ptrOutputVec4Float = 0; + std::uint32_t ptrInputVec3Float = 0; + std::uint32_t ptrOutputVec3Float = 0; std::uint32_t glIn = 0; std::uint32_t glOut = 0; std::uint32_t clipIn = 0; @@ -422,25 +486,42 @@ RectListShaders BuildRectListShaders(const RecompileResult& vertex, const Recomp require(target.spirvVersion >= 0x00010300u && target.spirvVersion <= 0x00010500u, "unsupported SPIR-V target version"); require(std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityTessellation) != target.supportedCapabilities.end(), "tessellation capability is unavailable"); std::vector parameters; + Emulation emulation; + BarycentricEmulationLayout layout; + if (fragment.barycentricEmulation.active) { + layout = LayoutBarycentricEmulation(fragment.fragmentParameters, fragment.barycentricEmulation); + for (const auto location : {layout.smoothLocation, layout.linearLocation}) { + if (location != BarycentricEmulationLayout::NoLocation) emulation.barycentrics.push_back(location); + } + } std::set locations; for (const auto& input : fragment.fragmentParameters) { require(input.location < 32 && input.sourceLocation < 32 && locations.insert(input.location).second, "invalid fragment parameter location"); require(!input.custom, "custom per-vertex interpolation is unsupported"); const bool exported = std::find(vertex.parameterExports.begin(), vertex.parameterExports.end(), input.sourceLocation) != vertex.parameterExports.end(); + if (fragment.barycentricEmulation.active && input.perVertex) { + require(exported, "an emulated per-vertex parameter is not exported by the vertex shader"); + const auto relocated = std::find_if(layout.perVertexLocations.begin(), layout.perVertexLocations.end(), [&](const auto& entry) { return entry.first == input.location; }); + require(relocated != layout.perVertexLocations.end(), "an emulated per-vertex parameter has no location"); + emulation.perVertex.push_back({input.sourceLocation, relocated->second}); + continue; + } parameters.push_back({input.sourceLocation, input.location, input.flat, !exported}); } + const auto emulatedSlots = static_cast(emulation.barycentrics.size() + emulation.perVertex.size() * 3u); const auto& limits = *target.tessellation; const std::uint32_t perVertexComponents = std::min({limits.maxControlPerVertexInputComponents, limits.maxControlPerVertexOutputComponents, limits.maxEvaluationInputComponents, limits.maxEvaluationOutputComponents}); - if (const std::size_t capacity = perVertexComponents / 4u > 0u ? perVertexComponents / 4u - 1u : 0u; parameters.size() > capacity) { + require(emulatedSlots == 0u || perVertexComponents / 4u > emulatedSlots, "emulated barycentrics exceed the tessellation interface limits"); + if (const std::size_t capacity = perVertexComponents / 4u > emulatedSlots ? perVertexComponents / 4u - 1u - emulatedSlots : 0u; parameters.size() > capacity) { parameters.resize(capacity); } - const auto components = static_cast((parameters.size() + 1) * 4); + const auto components = static_cast((parameters.size() + emulatedSlots + 1) * 4); require(limits.maxPatchSize >= 4 && components <= limits.maxControlPerVertexInputComponents && components <= limits.maxControlPerVertexOutputComponents && components <= limits.maxEvaluationInputComponents && components <= limits.maxEvaluationOutputComponents && limits.maxControlPerPatchOutputComponents >= 6 && components * 4 + 6 <= limits.maxControlTotalOutputComponents, "tessellation interface exceeds device limits"); const auto clipDistances = VertexClipDistanceCount(vertex.spirv.Words()); require(clipDistances == 0u || std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), spv::CapabilityClipDistance) != target.supportedCapabilities.end(), "the vertex clip distances need shaderClipDistance"); const auto faultBinding = RuntimeAbi::BindingNumber(RuntimeAbi::Stage::TessellationControl, RuntimeAbi::Binding::FaultBuffer); - RectListEmitter control(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding, clipDistances); - RectListEmitter evaluation(parameters, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding, clipDistances); + RectListEmitter control(parameters, emulation, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding, clipDistances); + RectListEmitter evaluation(parameters, emulation, spv::ExecutionModelTessellationEvaluation, target.spirvVersion, faultBinding, clipDistances); RectListShaders shaders; shaders.control.spirv = control.EmitControl(0u); shaders.control.bindings.push_back({DescriptorKind::StorageBuffer, DescriptorRole::FaultBuffer, 0, faultBinding, 1, {}}); @@ -452,7 +533,7 @@ RectListShaders BuildRectListShaders(const RecompileResult& vertex, const Recomp #endif shaders.lastVertexControl = shaders.control; if (std::any_of(parameters.begin(), parameters.end(), [](const Parameter& parameter) { return parameter.flat && !parameter.missing; })) { - RectListEmitter lastVertexControl(parameters, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding, clipDistances); + RectListEmitter lastVertexControl(parameters, emulation, spv::ExecutionModelTessellationControl, target.spirvVersion, faultBinding, clipDistances); shaders.lastVertexControl.spirv = lastVertexControl.EmitControl(2u); #if ANYPS5_ENABLE_SPIRV_TOOLS shaders.lastVertexControl.spirv = ValidateAndOptimizeSpirv(shaders.lastVertexControl.spirv, target.vulkanVersion, target.spirvVersion); From 9b84f9d5a191850853785d9a9b2a1d71648dc310 Mon Sep 17 00:00:00 2001 From: battleblob1 Date: Mon, 19 Oct 2026 22:07:30 +0300 Subject: [PATCH 026/318] fix(recompiler): replay stencil exports of pixel shaders that only read through BDA StencilReplaySafe refused every program with usesDma, including pixel shaders whose only BDA accesses are reads (Onimusha: Way of the Sword, PPSA27836: one global load, then image and buffer reads). Replaying such a shader per reference value is idempotent: its only side effect is the first-fault record, a compare-exchange that keeps the first fault; a fault is never retried but thrown by CheckFault, and replayed draws with a BDA table already wait and check faults before publishing. Written-page tracking exists only in programs with bdaWrites, which stay refused. Compiled shaders now carry bdaWrites, and ShadersWriteMemory counts a BDA page table as a memory write only for a program that writes through it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp | 4 +++- core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp | 8 ++++++++ core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp | 2 ++ core/shader/recompiler/Recompiler.cpp | 2 ++ core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 2 ++ core/shader/recompiler/StencilReplay.cpp | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 8 files changed, 20 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index eaca4be3f4..610112905c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -154,8 +154,10 @@ bool ShadersWriteMemory(std::span shaders) { if (binding.kind == ShaderRecompiler::DescriptorKind::StorageImage && any(binding.imageWritten, binding.count)) return true; break; case ShaderRecompiler::DescriptorRole::Gds: - case ShaderRecompiler::DescriptorRole::BdaPagetable: return true; + case ShaderRecompiler::DescriptorRole::BdaPagetable: + if (shader.program->bdaWrites) return true; + break; case ShaderRecompiler::DescriptorRole::GuestSamplers: case ShaderRecompiler::DescriptorRole::FaultBuffer: case ShaderRecompiler::DescriptorRole::FlattenedSrt: diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index ff2a3e40a7..f4b855ef84 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -1066,6 +1066,14 @@ int main() { return StencilReplaySafe(b.program); }); + passed &= run("read-only BDA reads can be replayed and BDA writes cannot", [] { + Builder b; + b.program.Info().usesDma = true; + if (!StencilReplaySafe(b.program)) return false; + b.program.Info().bdaWrites = true; + return !StencilReplaySafe(b.program); + }); + passed &= run("only guarded read-only SRT diagnostics can be replayed", [] { Builder b; b.program.Info().usesFaultBuffer = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index c86a452b79..b99f4abb0f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -111,6 +111,7 @@ void requireSameArtifact(const CompiledShaderArtifact& left, const CompiledShade require(left.parameterExports == right.parameterExports, prefix + "parameter exports differ"); require(left.stencilReferences == right.stencilReferences, prefix + "stencil reference set differs"); require(left.stencilReplaySafe == right.stencilReplaySafe, prefix + "stencil replay proof differs"); + require(left.bdaWrites == right.bdaWrites, prefix + "BDA write flag differs"); require(left.stencilReferenceDword == right.stencilReferenceDword, prefix + "stencil replay push offset differs"); require(left.fragmentParameters.size() == right.fragmentParameters.size(), prefix + "fragment parameter count differs"); for (std::size_t i = 0; i < left.fragmentParameters.size(); ++i) { @@ -221,6 +222,7 @@ RecompileResult sampleResult() { result.fragmentParameters = {{0, 1, true, false, true}, {2, 3, false, true}}; result.stencilReferences = {0u, 17u, 255u}; result.stencilReplaySafe = true; + result.bdaWrites = true; result.stencilReferenceDword = 23u; result.poisonedSrtReads = 3; result.shaderDataUserWords = {2u, 0xffffffffu, 9u}; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 48c22f5a9f..ce7793b96c 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -626,6 +626,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) result.registrationModule = registration; if (request.shader.stage == ShaderStage::Mesh && request.graphics) result.meshBarycentrics = request.graphics->meshBarycentrics; result.stencilReplaySafe = (request.shader.stage == ShaderStage::Vertex || stencilReplay) && StencilReplaySafe(program); + result.bdaWrites = program.Info().bdaWrites; if (stencilReplay) { result.stencilReferences = StencilReferenceValues(program, request.context.pixel.has_value() && request.context.pixel->stencilOperationValueExport ? 8u : 0u); result.stencilReferenceDword = bindings.layout.stencilReferenceDword; @@ -972,6 +973,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .meshBarycentrics = artifact.meshBarycentrics, .stencilReferences = artifact.stencilReferences, .stencilReplaySafe = artifact.stencilReplaySafe, + .bdaWrites = artifact.bdaWrites, .stencilReferenceDword = artifact.stencilReferenceDword, .variantId = artifact.variantId }; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5b1e075939..d77c2dbbe8 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -665,6 +665,7 @@ struct CompiledShaderArtifact { std::optional meshBarycentrics; std::vector stencilReferences; bool stencilReplaySafe = false; + bool bdaWrites = false; std::uint32_t stencilReferenceDword = 0xffffffffu; std::uint64_t variantId = 0; }; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 3a3d947b8c..f2a4683a1f 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -520,6 +520,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { } writer.Values(std::span(result.stencilReferences)); writer.Value(result.stencilReplaySafe); + writer.Value(result.bdaWrites); writer.Value(result.stencilReferenceDword); } @@ -582,6 +583,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { } reader.Values(result.stencilReferences); reader.Value(result.stencilReplaySafe); + reader.Value(result.bdaWrites); reader.Value(result.stencilReferenceDword); result.variantId = 0; } diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp index fae2057358..6ed182542f 100644 --- a/core/shader/recompiler/StencilReplay.cpp +++ b/core/shader/recompiler/StencilReplay.cpp @@ -692,7 +692,7 @@ bool StencilReferenceDefinedForKeptInvocations(const IrProgram& program) { } bool StencilReplaySafe(const IrProgram& program) { - if (program.Info().sharedMemoryBytes != 0u || program.Info().bdaWrites || program.Info().usesDma || (program.Info().usesFaultBuffer && program.Resources().guardedSrtSlots.empty())) return false; + if (program.Info().sharedMemoryBytes != 0u || program.Info().bdaWrites || (program.Info().usesFaultBuffer && program.Resources().guardedSrtSlots.empty())) return false; if (program.Info().usesFaultBuffer && !FaultControlFlowAcyclic(program)) return false; for (const auto& input : program.Info().inputs) if (input.kind == StageInputKind::PrimitiveId) return false; bool masks = false; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index f756ed2ade..0085c77723 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -63,7 +63,7 @@ - [Initial stencil state](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) uses zero for `DB_STENCIL_CONTROL`, `DB_STENCILREFMASK` and `DB_STENCILREFMASK_BF` (context `0x10b`-`0x10d`), including after `CLEAR_STATE`. The public V0, V4, V5 and V7-V11 [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) supply these values. Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h) agrees on the control register but uses `0x01000000` for the reference masks; the console reset values have not been measured. Planet Coaster (PPSA01736 v01.017) enables stencil tests without writing the control register. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. -- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. +- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. - [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. From 1713855fb6d019b3a1fa5dc99a94e719dda0355d Mon Sep 17 00:00:00 2001 From: David Date: Tue, 20 Oct 2026 14:22:01 +0400 Subject: [PATCH 027/318] docs(debt): record barycentric emulation in rect-list draws --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 2dee38e74e..31589adf92 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -93,7 +93,7 @@ - [Mesh layer output](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a mesh shader writes each primitive's layer through a one-member `Block` struct array whose member is decorated `BuiltIn Layer` and `PerPrimitiveEXT`, as glslang declares `gl_Layer` inside `gl_MeshPerPrimitiveEXT`, not through a standalone `PerPrimitiveEXT` `Layer` array. Both forms pass `spirv-val`, but AMD's Windows driver (32.0.21045.5002, RX 6700 XT) crashes in `amdvlk64.dll` creating a pipeline whose mesh shader writes the standalone form, whether the layer is signed or unsigned and whatever the target, while the block form draws, also with a pixel shader that reads the layer. - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64 or, on a device without the Float64 capability, in 64-bit integers: the exact product is added with a sticky bit and rounded once. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the integer arithmetic on every device, and the f64 arithmetic on devices with Float64. It checks both against the hardware rows in every mode, and against a CPU reference on random rows in every mode and f16 rounding. -- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. Tessellation, mesh and rect-list pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. +- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the first vertex's value there, as the geometry stage's strip gives it; the last provoking vertex throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. A rect-list draw takes them from its own generated tessellation stages, checked on MoltenVK 1.4.2 (Apple M2) only. Tessellation and mesh pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. - [v_floor_f64, v_ceil_f64, v_fract_f64, v_frexp_mant_f64 and v_frexp_exp_i32_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field: a denormal input is flushed to a zero of its sign when bit 0 is clear (0 and 2), and `v_fract_f64` flushes a denormal result, the fraction of a positive denormal input, to +0 when bit 1 is clear (0 and 1). `v_trunc_f64` and `v_rndne_f64` give the same results either way and are unchanged. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 23 f64 values (zeros, denormals, minimum normals, ±0.5, ±1, values next to 1, the largest finite value, infinities and quiet and signaling NaNs) and 16 of them with neg and abs modifiers. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_add_f64 and v_mul_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field; the PS5's gfx1013 is not measured. Inputs flush to a zero of their sign when bit 0 is clear (0 and 2). When bit 1 is clear (0 and 1), a sum below 2^-1022, or a product whose exact value rounded to 53 significant bits with an unbounded exponent is below 2^-1022, flushes to a zero of its sign, so `0x2000000000000001 * 0x1ffffffffffffffe` (exactly 2^-1022 - 2^-1126) rounds to 2^-1022 and is kept. Measured on RDNA2 (gfx1031, RX 6700 XT, AMD Windows driver 32.0.21045.5002, HIP) at IEEE=0 and 1, denorm16 0..3, over 576 pairs of 24 f64 values (zeros, denormals, minimum normals, values next to 1 and to 2^-1022, products just below 2^-1022, the largest finite value, infinities), with neg, abs and clamp. `v_fma_f64`, `v_div_fmas_f64` and shaders without a known float mode keep the previous lowering. From 9d4c7cf42b48c40dfa8265025f2a022f1c8244e7 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 20 Oct 2026 10:10:38 +0000 Subject: [PATCH 028/318] fix(shader): share the words of identical specialized modules specializeModule caches a module per artifact variant and constant set, so artifacts whose SPIR-V is the same and that get the same constants each build and keep their own copy of the same specialized words. The built words are now interned by content (both content hashes, then the words themselves), and later equal modules share one copy. Each module keeps its own specialization ID. In Crimson Desert, 4500 specialized modules built in the first 235 s held 916.6 MiB of words, of which 133.5 MiB were distinct. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 19 +++++++++++++++++++ core/shader/recompiler/Recompiler.cpp | 16 +++++++++++++++- core/shader/recompiler/Recompiler.hpp | 2 ++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 1be75623e3..9f545033da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -1,5 +1,7 @@ #include "SpirvBackend/SpirvModule.hpp" #include "SpirvBackend/SpirvSpecialization.hpp" +#include "Recompiler.hpp" +#include #include #include #include @@ -356,6 +358,22 @@ void testBlockBallotsOfOnePredicateShareOneBallot() { check(ballotSources.contains(afterLabel), "a ballot in a later block reused one from an earlier block"); } +void testInternedSpirvSharesEqualWords() { + const std::vector words{spv::MagicNumber, 0x00010400u, 0u, 7u, 0u, (2u << 16u) | spv::OpCapability, spv::CapabilityShader}; + auto changed = words; + changed.back() = spv::CapabilityInt64; + const auto first = InternSpirv(words); + const auto second = InternSpirv(words); + const auto other = InternSpirv(changed); + check(std::ranges::equal(first.Words(), words) && std::ranges::equal(other.Words(), changed), "interned SPIR-V changed its words"); + check(first.data() == second.data(), "two equal specialized modules kept separate copies of their words"); + check(first.data() != other.data(), "different SPIR-V was interned into one copy"); + auto edited = second; + edited[6] = spv::CapabilityFloat64; + const auto again = InternSpirv(words); + check(edited[6] == spv::CapabilityFloat64 && first[6] == spv::CapabilityShader && again.data() == first.data(), "a write to one holder of interned SPIR-V changed the others"); +} + int main() { testFoldingLeavesNoDeadBlocksOrConstants(); testIdsBeyondTheBoundAreRejected(); @@ -371,6 +389,7 @@ int main() { testIdsStayUniqueBeyondCacheCapacity(); testEmittedWordsAreStable(); testBlockBallotsOfOnePredicateShareOneBallot(); + testInternedSpirvSharesEqualWords(); if (failures != 0) { std::fprintf(stderr, "%d check(s) failed\n", failures); return 1; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 48c22f5a9f..4e02bd10c5 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -808,7 +808,7 @@ std::shared_ptr buildSpecializedModule(const CompiledSh } cursor += count; } - module->spirv = std::move(materialized); + module->spirv = InternSpirv(std::move(materialized)); module->specializationId = constants.empty() ? 0u : nextVariantId(); return module; } @@ -1129,6 +1129,20 @@ bool sameLayout(const BindingLayout& left, const BindingLayout& right) { } +SharedSpirv InternSpirv(std::vector words) { + SharedSpirv spirv(std::move(words)); + const auto hash = spirv.ContentHash(); + static std::mutex mutex; + static std::unordered_map> interned; + std::lock_guard lock(mutex); + auto& candidates = interned[hash.low ^ (hash.high * 0x9e3779b97f4a7c15ull)]; + for (const auto& candidate : candidates) { + if (candidate.ContentHash() == hash && candidate == spirv) return candidate; + } + candidates.push_back(spirv); + return spirv; +} + MeshOutputBudget MeshBarycentricOutputBudget(const MeshConfiguration& mesh, const MeshTargetLimits& limits, const MeshOutputUse& use) { const auto alignUp = [](std::uint64_t value, std::uint64_t granularity) { return granularity <= 1u ? value : (value + granularity - 1u) / granularity * granularity; }; MeshOutputBudget budget; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5b1e075939..ee81dc83b1 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -622,6 +622,8 @@ private: std::shared_ptr holder; }; +[[nodiscard]] SharedSpirv InternSpirv(std::vector words); + struct VertexInput { std::uint32_t location; std::uint32_t components; From 7feb59518002444844a6880286d81d8749405102 Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:07:34 -0300 Subject: [PATCH 029/318] fix(relinker): treat guest DT_INIT_ARRAY without DT_INIT_ARRAYSZ as empty A guest module with DT_INIT_ARRAY or DT_FINI_ARRAY and no size tag stopped with "Incomplete guest lifecycle array". FreeBSD rtld reads the entry count only from the size tag, so such an array calls nothing; read it as empty, as code analysis does for the executable. A size without an array still fails. --- .../relinker/src/guest/GuestImageReader.cpp | 4 ++-- .../relinker/tests/test_guest_none_relocations.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 3e9a239afa..2c7a78c150 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -276,8 +276,8 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (image.Init != 0) mapped(image.Init, 1, 1); if (image.Fini != 0) mapped(image.Fini, 1, 1); const auto readLifecycleArray = [&](std::uint64_t addressTag, std::uint64_t sizeTag, std::vector& entries) { - if (!tags.contains(addressTag) && !tags.contains(sizeTag)) return; - if (!tags.contains(sizeTag) || (tags.at(sizeTag) != 0 && !tags.contains(addressTag))) fail("Incomplete guest lifecycle array"); + if (!tags.contains(sizeTag)) return; + if (tags.at(sizeTag) != 0 && !tags.contains(addressTag)) fail("Incomplete guest lifecycle array"); const auto size = tags.at(sizeTag); if (size == 0) return; if (size % 8 != 0) fail("Unaligned guest lifecycle array size"); diff --git a/core/relinker/relinker/tests/test_guest_none_relocations.py b/core/relinker/relinker/tests/test_guest_none_relocations.py index 44d55ed646..9ebc28509d 100644 --- a/core/relinker/relinker/tests/test_guest_none_relocations.py +++ b/core/relinker/relinker/tests/test_guest_none_relocations.py @@ -76,6 +76,11 @@ def change_tag(image, tag, value=None, replacement=None): raise AssertionError(f"Missing dynamic tag {tag:#x}") +def unsized_lifecycle(rela, sce, initial): + image = module(rela, sce=sce, lifecycle=True, initial=initial) + return change_tag(change_tag(image, 27, replacement=0x61000001), 28, replacement=0x61000001) + + def check_output(data, system, rela, plt, initial): expected_rela = [(entry[0], entry[1] & 0xffffffff, entry[2]) if (entry[1] & 0xffffffff) == 8 and entry[1] >> 32 else entry @@ -123,6 +128,7 @@ def invalid_cases(sce): entry = 0x61000033 if sce else 9 address = 0x6100002f if sce else 7 plt_format = 0x6100002b if sce else 20 + yield "lifecycle-without-address", change_tag(module([RELATIVE], sce=sce, lifecycle=True, initial=0), 25, replacement=0x61000001), "Incomplete guest lifecycle array" yield "partial-entry", change_tag(module([NOOP], sce=sce), size, 23), "Invalid relocation table size" yield "entry-size", change_tag(module([NOOP], sce=sce), entry, 16), "Invalid RELA entry size" yield "unmapped-table", change_tag(module([NOOP], sce=sce), address, 0xffffffffffffffff), ("SCE table exceeds" if sce else "Unmapped ELF address") @@ -186,6 +192,15 @@ def main(): executed = subprocess.run([sys.executable, __file__, "--load", str(artifact)], capture_output=True, text=True, timeout=10) assert executed.returncode == 0, (executed.stdout, executed.stderr) + for name, rela, initial in (("unsized-lifecycle", [RELATIVE], UNCHANGED), + ("unsized-lifecycle-unread", [(TARGET, 0, FUNCTION)], FUNCTION)): + image = unsized_lifecycle(rela, sce, initial) + for system in ("linux", "windows", "macos"): + result, artifact, output = convert(f"{sce}-{name}", system, image) + assert result.returncode == 0, (sce, name, system, result.stdout, result.stderr) + check_output(artifact.read_bytes(), system, rela, [], initial) + if system == "linux": + assert not {25, 26, 27, 28} & set(dynamic_tags(artifact.read_bytes())), name for name, image, error in invalid_cases(sce): result, artifact, output = convert(f"{sce}-{name}", "linux", image) assert result.returncode == 2 and error in result.stderr, (sce, name, result.stderr) From bbf532b638d5744371fb63a35f2c0a3d5a6ddc29 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 09:31:29 -0300 Subject: [PATCH 030/318] fix(agc): read capture output directories through the wide environment on Windows Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Execution/src/SubmissionCapture.cpp | 6 +++++- .../prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 8 +++++++- core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp | 6 +++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp index 217e5b0c81..9caddde7bd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/SubmissionCapture.cpp @@ -101,8 +101,12 @@ SubmissionCapture::SubmissionCapture(const std::filesystem::path& directory) : d SubmissionCapture* SubmissionCapture::Active() { static SubmissionCapture* const capture = [] { +#ifdef _WIN32 + const wchar_t* directory = _wgetenv(L"APS5_DUMP_SUBMITS"); +#else const char* directory = std::getenv("APS5_DUMP_SUBMITS"); - return directory != nullptr && *directory != '\0' ? new SubmissionCapture(directory) : nullptr; +#endif + return directory != nullptr && *directory != 0 ? new SubmissionCapture(directory) : nullptr; }(); return capture; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 53d7e239fa..cb02125e6f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -126,6 +126,12 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, PerformanceCounters::CountPipeline(); } +#ifdef _WIN32 +const wchar_t* ComputePipelineDumpDirectory() { return _wgetenv(L"APS5_DUMP_COMPUTE_PIPELINES"); } +#else +const char* ComputePipelineDumpDirectory() { return std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); } +#endif + bool CaptureComputePipeline(std::size_t words) { const char* minimum = std::getenv("APS5_DUMP_COMPUTE_MIN_WORDS"); if (minimum == nullptr) return true; @@ -4885,7 +4891,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons const Graphics::PipelineSpecialization specialization(shader); const auto subgroupSize = computeSubgroupSize(shader); const auto stageFlags = computeStageFlags(shader); - if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && ComputePipelineCapture::OptionalEnabled() && CaptureComputePipeline(shader.spirv.size())) { + if (const auto* directory = ComputePipelineDumpDirectory(); directory != nullptr && *directory != 0 && ComputePipelineCapture::OptionalEnabled() && CaptureComputePipeline(shader.spirv.size())) { ComputePipelineCapture capture; capture.spirv = shader.spirv.Copy(); ComputeDescriptorSetLayout resourcesLayout; diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp index 98cf833fd3..8668696b41 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4Replay.cpp @@ -693,8 +693,12 @@ void testCorrupt(const std::filesystem::path& directory) { int main() { std::filesystem::path scratch; try { +#ifdef _WIN32 + const wchar_t* captured = _wgetenv(L"APS5_DUMP_SUBMITS"); +#else const char* captured = std::getenv("APS5_DUMP_SUBMITS"); - check(captured != nullptr && *captured != '\0', "set APS5_DUMP_SUBMITS to the capture directory of this test"); +#endif + check(captured != nullptr && *captured != 0, "set APS5_DUMP_SUBMITS to the capture directory of this test"); const std::filesystem::path directory = captured; std::filesystem::remove_all(directory); scratch = std::filesystem::temp_directory_path() / ("anyps5-pm4-replay-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); From 96ff1330eb04c027750a183c555fa9ee51311b62 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 20 Oct 2026 15:01:44 +0200 Subject: [PATCH 031/318] fix(agc): leave the float-controls probe's pipelines out of compute captures The start-up f32 denormal flush probe already stays out of the pipeline counters and the pipeline journal, but APS5_DUMP_COMPUTE_PIPELINES still captured its pipelines. On a device that runs the probe, every capture directory gained twelve driver-internal records that count toward the 128-record limit, and agc_compute_pipeline_capture_run found 14 captures for a fixture with two programs. --- core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 2 +- docs/dev/BUILD.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 53d7e239fa..9e193faa41 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4885,7 +4885,7 @@ std::shared_ptr VulkanDevice::createComputePipeline(cons const Graphics::PipelineSpecialization specialization(shader); const auto subgroupSize = computeSubgroupSize(shader); const auto stageFlags = computeStageFlags(shader); - if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && ComputePipelineCapture::OptionalEnabled() && CaptureComputePipeline(shader.spirv.size())) { + if (const char* directory = std::getenv("APS5_DUMP_COMPUTE_PIPELINES"); directory != nullptr && *directory != '\0' && !state->probingFloatControls && ComputePipelineCapture::OptionalEnabled() && CaptureComputePipeline(shader.spirv.size())) { ComputePipelineCapture capture; capture.spirv = shader.spirv.Copy(); ComputeDescriptorSetLayout resourcesLayout; diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index b1b725a14f..7f3a6de87f 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -151,7 +151,7 @@ On Windows MinGW frees `thread_local` objects before their destructors run, so t ## Compute pipeline replay -Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. Capture stops with an error at 128 records, 256 MiB total or 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. +Build `agc_compute_pipeline_replay` in the full build with `-DANYPS5_ENABLE_SPIRV_TOOLS=ON`; replay validates the captured module before calling Vulkan. Set `APS5_DUMP_COMPUTE_PIPELINES` to a dedicated empty directory before starting a title to save each uncached compute pipeline's final SPIR-V, descriptor layouts, push constant ranges, specialization values, subgroup size and creation flags before compilation. Files are named `compute-/capture.aps5cp`; identical records are deduplicated. The pipelines of the start-up f32 denormal flush probe are not captured. Capture stops with an error at 128 records, 256 MiB total or 36 MiB per record (at most 8,388,608 SPIR-V words, or 32 MiB of shader code). Capture files contain shader code and device metadata, but no guest buffers or textures; review them before sharing. Set `APS5_DUMP_COMPUTE_MIN_WORDS` to capture only modules at or above that SPIR-V word count, for example `100000` when investigating large NVIDIA compilations. The default is `0` (all modules); values outside `0` through `8388608` fail. The filter applies only when capture is enabled. From f923961c6644ad894944952b27762674fd77b18c Mon Sep 17 00:00:00 2001 From: Daniele Giovane <77807986+yabanana@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:42:39 +0200 Subject: [PATCH 032/318] fix(shader): reject temporary owners for borrowed memory regions --- core/shader/recompiler/Recompiler.hpp | 14 ++++- .../tests/RequestSerializerTests.cpp | 59 +++++++++++++++++++ docs/dev/debt/shader.md | 2 + 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 5b1e075939..7f540589b0 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,17 @@ struct MemoryRegion { std::span bytes; }; +class MemoryRegionView : public std::span { + using Base = std::span; +public: + using Base::Base; + constexpr MemoryRegionView() noexcept = default; + constexpr MemoryRegionView(Base value) noexcept : Base(value) {} + template + requires (!std::ranges::borrowed_range) + MemoryRegionView(TRange&&) = delete; +}; + struct ShaderBinary { ShaderStage stage; std::uint64_t codeAddress; @@ -236,7 +248,7 @@ struct GuestContext { std::optional compute; std::optional pixel; std::optional vertex; - std::span memory; + MemoryRegionView memory; std::optional floatMode; std::span linkedCallFallbacks; bool convertedSamples = false; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index be4b2a16c7..55215ac897 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include namespace { @@ -45,6 +47,62 @@ void Require(bool condition) { if (!condition) std::abort(); } +template +concept AssignableMemory = requires(ShaderRecompiler::GuestContext& context, TRange&& range) { + context.memory = std::forward(range); +}; + +using Region = ShaderRecompiler::MemoryRegion; +using Regions = std::vector; +using RegionArray = std::array; +using RegionView = ShaderRecompiler::MemoryRegionView; + +static_assert(!AssignableMemory); +static_assert(!AssignableMemory); +static_assert(!AssignableMemory); +static_assert(!AssignableMemory>); +static_assert(AssignableMemory && AssignableMemory); +static_assert(AssignableMemory && AssignableMemory); +static_assert(AssignableMemory> && AssignableMemory); +static_assert(sizeof(RegionView) == sizeof(std::span)); +static_assert(alignof(RegionView) == alignof(std::span)); +static_assert(std::is_trivially_copyable_v); +static_assert(std::is_nothrow_constructible_v>); +static_assert([] { + const RegionArray owned{{{0x1000u, {}}}}; + const RegionView view{std::span(owned)}; + return view.size() == 1u && view.front().guestAddress == 0x1000u; +}()); + +void VerifyMemoryView() { + const std::array bytes{std::byte{1}, std::byte{2}, std::byte{3}, std::byte{4}}; + Regions owned{{0x1000u, bytes}}; + const Regions frozen = owned; + Region array[]{{0x2000u, bytes}}; + RegionArray standard{{{0x3000u, bytes}}}; + ShaderRecompiler::RecompileRequest request{}; + request.context.memory = owned; + Require(request.context.memory.data() == owned.data()); + owned.front().guestAddress = 0x4000u; + Require(request.context.memory.front().guestAddress == 0x4000u); + request.context.memory = frozen; + Require(request.context.memory.data() == frozen.data()); + request.context.memory = array; + Require(request.context.memory.data() == array); + request.context.memory = standard; + Require(request.context.memory.data() == standard.data()); + request.context.memory = std::span(owned).first(1); + const auto copy = request; + Require(copy.context.memory.data() == owned.data() && copy.context.memory.front().bytes.data() == bytes.data()); + request.context.memory = {owned.data(), owned.size()}; + const ShaderRecompiler::RequestSerializer serializer; + const auto replay = serializer.Deserialize(serializer.Serialize(request)); + Require(replay.request.context.memory.size() == 1u && replay.request.context.memory.front().guestAddress == 0x4000u); + Require(std::ranges::equal(replay.request.context.memory.front().bytes, bytes)); + request.context.memory = {}; + Require(request.context.memory.empty()); +} + void InsertMeshBarycentricFlag(std::vector& bytes) { constexpr std::array draw{0x00, 0x34, 0xef, 0xcd, 0xab, 0x00, 0x00, 0x00, 57u, 0u, 0u, 0u, 4u, 0u, 0u, 0u, 9u, 0u, 0u, 0u}; const auto at = std::search(bytes.begin(), bytes.end(), draw.begin(), draw.end()); @@ -94,6 +152,7 @@ std::string EncodeBase64(const std::vector& bytes) { } int main() { + VerifyMemoryView(); const ShaderRecompiler::RequestSerializer serializer; const auto historical = serializer.Deserialize(kHistoricalVersion21); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index fc987190ec..824da14782 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -2,6 +2,8 @@ ### Build +- [GuestContext memory](../../../core/shader/recompiler/Recompiler.hpp) rejects direct construction from temporary owning ranges. The view does not own the region array or its referenced bytes. That storage must outlive the request and its consumers, including when supplied through named containers, existing spans or pointers. + - winpthreads initializes a statically initialized rwlock on its first lock and fails a concurrent first lock with `EINVAL`, which `std::shared_mutex` and `std::shared_timed_mutex` ignore, so that lock does not exclude. The host `std::shared_mutex` objects in [Recompiler.cpp](../../../core/shader/recompiler/Recompiler.cpp) are locked once when created. A new host `std::shared_mutex` used on Windows needs the same. - The bundled [SPIRV-Tools build](../../../core/shader/recompiler/PatchSpirvTools.cmake) replaces the redundancy-elimination source with a build-directory copy. A depth-first traversal keeps one value map and removes each subtree's added values on return, avoiding growing map copies and preventing sibling values from leaking into one another. This extends the processed-map release fix in [SPIRV-Tools #6962](https://github.com/KhronosGroup/SPIRV-Tools/pull/6962). Remove the patch when the pinned dependency has equivalent copy-free traversal; #6962 alone still copies maps for sibling branches. System-dependency builds need an equivalent fix in their installed package. Optimization passes and validation remain unchanged; wider shader and game performance measurement is needed. From fa61ee5dc820d7e5b35d30b88a9896c86e336c0c Mon Sep 17 00:00:00 2001 From: billythemenace2 Date: Sat, 17 Oct 2026 22:40:54 +0100 Subject: [PATCH 033/318] fix(agc): import registered ranges larger than the device's largest allocation in pieces An address-based build imported each registered range whole. A range larger than maxMemoryAllocationSize (2 GiB on AMD's Windows driver) failed at vkAllocateMemory, and the build then asked for a heap mirror of the same size, which failed too. Such a range is now imported in pieces no larger than the limit, built from the range window's cells, each one BDA table entry. Single imports above the limit are no longer attempted. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Execution/src/VulkanDevice.cpp | 4 +- .../Graphics/include/GuestBufferMemory.hpp | 1 + .../Graphics/src/GuestBufferMemory.cpp | 48 +++++++++++++++++++ .../libSceAgcDriver/tests/BdaResources.cpp | 29 +++++++++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c19c2efa29..55d450fd1c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1390,7 +1390,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : // Guest memory is host memory: importing it lets address-based shaders use it in place instead of // copying every registered allocation per draw. if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) && std::getenv("APS5_NO_HOST_IMPORT") == nullptr) { - VkPhysicalDeviceDriverProperties driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES}; + VkPhysicalDeviceMaintenance3Properties maintenance3{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_3_PROPERTIES}; + VkPhysicalDeviceDriverProperties driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES, &maintenance3}; VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT, &driverProperties}; VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties}; state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &properties); @@ -1402,6 +1403,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : #endif state->drawInputsInPlace = std::getenv("APS5_NO_DRAW_INPUTS_IN_PLACE") == nullptr && (std::getenv("APS5_DRAW_INPUTS_IN_PLACE") != nullptr || DrawInputsInPlaceUsable(driverProperties.driverID, windows)); if (!compileOnly) Graphics::SetHostImportWindow(8ull << 20u); + if (!compileOnly) Graphics::SetHostImportMaxBytes(maintenance3.maxMemoryAllocationSize); #ifdef _WIN32 // Imported pages are locked; locking is bounded by the process working-set minimum. if (const char* value = std::getenv("APS5_WORKING_SET_MIB"); value != nullptr && !compileOnly) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index ceec04cf3e..4b22397ee1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -113,6 +113,7 @@ void SetHostImportDeferredSettles(bool enabled); // The larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half // that heap: the other half stays for the buffers, mirrors and textures that share it. std::uint64_t HostImportBudget(const VkPhysicalDeviceMemoryProperties& memory, std::uint64_t physicalBytes); +void SetHostImportMaxBytes(std::uint64_t bytes); std::uint64_t HostImportLiveBytes(); struct HostImportSettleCounts { std::uint64_t submitted = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 56368f852f..4fa92664a4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1548,6 +1548,7 @@ std::string budgetSource(const Context& context, const HostImports& state, std:: } std::atomic rangeImportWindow{64ull << 20u}; +std::atomic importMaxBytes{0}; // The import of an access to a registered range larger than the range window: the cells the access touches, grown to // the union of the range's imports it overlaps, so accesses across neighbouring cell edges share one import instead of @@ -1654,6 +1655,32 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s return importRunAt(context, state, std::move(run), false, address, lease, end, evict, requester); } +struct ImportPiece { + std::uint64_t begin; + std::uint64_t end; + const HostImport* entry; +}; + +std::vector importPieces(const Context& context, HostImports& state, const GuestAllocations::Range& range, const GuestAllocations::Lease& lease) { + const auto limit = importMaxBytes.load(std::memory_order_relaxed); + const auto window = rangeImportWindow.load(std::memory_order_relaxed); + const auto alignment = context.hostImportAlignment; + if (limit < 2 * window || window == 0 || alignment == 0 || window % alignment != 0 || range.address % alignment != 0) return {}; + const auto step = (limit / window - 1) * window; + const auto end = range.address + range.bytes; + std::vector pieces; + for (auto cursor = range.address; cursor < end;) { + const auto wanted = end - cursor < step + window ? end : cursor + step; + const auto* entry = importAllocation(context, state, cursor, lease, ImportRequester::Build, wanted, false); + if (entry == nullptr || entry->base > cursor || entry->bytes > limit) return {}; + const auto to = std::min(end, entry->base + entry->bytes); + if (to <= cursor) return {}; + pieces.push_back({cursor, to, entry}); + cursor = to; + } + return pieces; +} + const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { if (const auto* low = peekImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base; if (const auto* high = peekImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes; @@ -1679,6 +1706,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import return &found->second; } if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; + if (const auto limit = importMaxBytes.load(std::memory_order_relaxed); limit != 0 && bytes > limit) return nullptr; if (const auto failed = failures.find(base); !stepDown && failed != failures.end()) { if (sameRun(failed->second, run)) return nullptr; failures.erase(failed); @@ -3379,6 +3407,10 @@ void SetHostImportRangeWindow(std::uint64_t bytes) { rangeImportWindow.store(bytes, std::memory_order_relaxed); } +void SetHostImportMaxBytes(std::uint64_t bytes) { + importMaxBytes.store(bytes, std::memory_order_relaxed); +} + void SetHostImportBudget(std::uint64_t bytes) { budgetOverride.store(bytes, std::memory_order_relaxed); auto& state = Imports(); @@ -3737,8 +3769,14 @@ void GuestBufferMemory::AcquireRegistered() { validate(range->address, range->bytes); Region region{range->address, range->address + range->bytes, range->writable, {}, nullptr}; const HostImport* entry = nullptr; + std::vector pieces; + const auto limit = importMaxBytes.load(std::memory_order_relaxed); if (importable && covering != nullptr && range->address >= coveredBegin && range->address + range->bytes <= coveredEnd) { entry = covering; + } else if (importable && limit != 0 && range->bytes > limit) { + auto& state = Imports(); + std::lock_guard lock(state.mutex); + pieces = importPieces(context, state, *range, lease); } else if (importable) { auto& state = Imports(); std::lock_guard lock(state.mutex); @@ -3750,6 +3788,16 @@ void GuestBufferMemory::AcquireRegistered() { } } lap(timing.importsUs, at); + if (!pieces.empty()) { + for (const auto& piece : pieces) { + Region part{piece.begin, piece.end, range->writable, {}, nullptr}; + part.hostBacked = true; + part.direct = piece.entry; + regions.push_back(std::move(part)); + } + imported.push_back(range->address); + continue; + } if (entry != nullptr) { region.hostBacked = true; // Reused by Upload while no import was dropped since (see importsEpoch). diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 6d625cf165..db20b0e55a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -905,6 +905,34 @@ void importRefusedWholeRangeTests(const Context& context) { Require(kept != nullptr && kept->buffer == buffer, "the range's window was imported again after a refused whole-range import"); } +void importPiecesOverAllocationLimitTests(const Context& context) { + WindowedRange range(context, 2 * WindowedRange::piece, 5); + const auto first = range.First(); + const auto limit = 3 * range.window; + SetHostImportMaxBytes(limit); + { + GuestBufferMemory leased(range.importing); + leased.AcquireRegistered(); + leased.Upload(true); + std::uint64_t covered = first; + for (const auto& entry : leased.AddressRanges()) { + if (entry.end <= first || entry.begin >= first + range.bytes) continue; + Require(entry.begin == covered, "the pieces of a range over the allocation limit leave a gap or overlap in the BDA table"); + Require(entry.end - entry.begin <= limit, "a BDA table entry of a range over the allocation limit is larger than the limit"); + const auto* piece = ExistingHostImportFor(range.importing, entry.begin, static_cast(entry.end - entry.begin)); + Require(piece != nullptr && piece->bytes <= limit && entry.deviceAddress == piece->address + (entry.begin - piece->base), "a BDA table entry of a range over the allocation limit is not served in place by an import within the limit"); + covered = entry.end; + } + Require(covered == first + range.bytes, "the pieces of a range over the allocation limit do not cover it"); + for (std::uint64_t at = first; at < first + range.bytes; at += range.window) { + const auto* piece = ExistingHostImportFor(range.importing, at, 16); + Require(piece != nullptr && piece->bytes <= limit, "a range over the allocation limit has an import larger than the limit"); + } + leased.WriteBack(); + } + SetHostImportMaxBytes(0); +} + // Two uses across neighbouring cell edges of one range, each in a build of its own, share one import: the second grows // the first's window to their union, and later rounds of both keep it. Before, each replaced the other's window at // every use. @@ -3364,6 +3392,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { importWholeRangeOverWindowTests(context); importStaleProbeTests(context, access); importRefusedWholeRangeTests(context); + importPiecesOverAllocationLimitTests(context); importNeighbouringEdgesTests(context); importAtMapOverBudgetTests(context); importRequesterTests(context, access); From c819b9b3f19d04643de985770a2dcc5c728a86a7 Mon Sep 17 00:00:00 2001 From: Longziip Date: Mon, 19 Oct 2026 23:53:18 +0100 Subject: [PATCH 034/318] fix(agc): keep storage image results recorded after a direct write when its completion stamps it again A dispatch's direct (host-imported) buffer writes are stamped when it is recorded, and stamped again by its completion write-back. A storage image written later in GPU order over the same memory saw the second stamp as a newer write and dropped its pending results on its next refresh, re-uploading the dispatch's older bytes. --- .../Graphics/include/GuestBufferMemory.hpp | 1 + .../Graphics/include/Texture.hpp | 2 + .../Graphics/src/GuestBufferMemory.cpp | 9 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 33 +++ .../tests/execution/DirectWriteCompletion.cpp | 224 ++++++++++++++++++ 5 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index ceec04cf3e..28d51d428a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -495,6 +495,7 @@ private: bool prepared = false; bool uploaded = false; bool committed = false; + mutable std::uint64_t directWritesGeneration = 0; }; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index f54483fcbd..c18c4ee8c9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -224,6 +225,7 @@ public: // any of the [begin, end) ranges: one acquisition of the registry's mutex. static bool AnyPendingOverlaps(std::span> ranges); static bool AnyLiveOverlaps(std::span> ranges); + static std::uint64_t StampKeepingNewerResults(std::uint64_t address, std::size_t bytes, std::uint64_t recorded, const std::function& stamp); // Moves with every change of the pending registry (an image marked dirty, stored, flushed or // dropped; after the store for a flush), bumped under the registry's mutex with the change, so // a reader that loads it before and after its own reads knows the registry it consulted stayed diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 56368f852f..76bf87ce85 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -5265,7 +5265,10 @@ void GuestBufferMemory::MarkDirectWrites() const { const auto* found = owner(begin, end); if (found == nullptr) continue; const auto& region = *found; - if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) GuestMemory::MarkWritten(begin, static_cast(end - begin)); + if (region.direct != nullptr || (region.gpuCopy && region.copiedBack)) { + const auto generation = GuestMemory::MarkWritten(begin, static_cast(end - begin)); + if (generation != 0) directWritesGeneration = std::max(directWritesGeneration, generation); + } } } @@ -5353,7 +5356,7 @@ void GuestBufferMemory::WriteBack() { if (region.direct == nullptr && !(region.gpuCopy && (region.copiedBack || region.deviceLocal)) && !(region.mirror != nullptr && !region.mirror->writable)) Recorder::NoteWrittenBack(begin, static_cast(end - begin)); if (region.direct != nullptr) { // The GPU wrote imported guest memory directly; page write watching did not see it. - GuestMemory::MarkWritten(begin, static_cast(end - begin)); + StorageTexture::StampKeepingNewerResults(begin, static_cast(end - begin), directWritesGeneration, [&] { return GuestMemory::MarkWritten(begin, static_cast(end - begin)); }); continue; } if (region.mirror != nullptr) { @@ -5383,7 +5386,7 @@ void GuestBufferMemory::WriteBack() { if (region.copiedBack) { // The GPU copied the written sub-range back into the import; page write watching // did not see it. - GuestMemory::MarkWritten(begin, static_cast(end - begin)); + StorageTexture::StampKeepingNewerResults(begin, static_cast(end - begin), directWritesGeneration, [&] { return GuestMemory::MarkWritten(begin, static_cast(end - begin)); }); continue; } static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a2d90b6926..e548e78b1b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4039,6 +4039,39 @@ bool PendingStorageOverlaps(std::uint64_t address, std::size_t bytes, const Stor return false; } +std::uint64_t StorageTexture::StampKeepingNewerResults(std::uint64_t address, std::size_t bytes, std::uint64_t recorded, const std::function& stamp) { + std::vector, std::uint32_t>> kept; + if (recorded != 0 && bytes != 0) { + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (auto* texture : pending.textures) { + if (!texture->blockUnits || !texture->overlaps(address, bytes)) continue; + auto alive = texture->weak_from_this().lock(); + if (alive == nullptr) continue; + for (std::uint32_t unit = 0; unit < texture->trackedLayers; ++unit) { + const auto begin = texture->layerBegin(unit); + const auto unitBytes = texture->layerBytes(unit); + if (!texture->layerPending[unit] || begin >= address + bytes || address >= begin + unitBytes) continue; + if (texture->layerGeneration[unit] >= recorded) kept.emplace_back(alive, unit); + } + } + } + std::erase_if(kept, [](const auto& entry) { + const auto& [texture, unit] = entry; + return !GuestMemory::UnchangedSince(texture->layerBegin(unit), static_cast(texture->layerBytes(unit)), texture->layerGeneration[unit]); + }); + const auto generation = stamp(); + if (generation == 0 || kept.empty()) return generation; + auto& pending = Pending(); + std::lock_guard lock(pending.mutex); + for (const auto& [texture, unit] : kept) { + if (!texture->layerPending[unit]) continue; + texture->layerGeneration[unit] = generation; + texture->refreshGeneration(); + } + return generation; +} + bool StorageTexture::AnyPendingOverlaps(std::span> ranges) { if (ranges.empty()) return false; auto& pending = Pending(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp new file mode 100644 index 0000000000..0a0d22a8bf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp @@ -0,0 +1,224 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::StorageTexture; +constexpr std::uint32_t Side = 256; +constexpr std::uint32_t Format32UInt = 20; +constexpr std::uint32_t TileR64KBX = 0x1b; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t Block = 0x10000; +constexpr std::size_t SurfaceBytes = 4 * Block; +constexpr std::uint32_t Untouched = 0xdeadbeefu; +constexpr std::uint32_t Stored = 0x12345678u; +constexpr std::uint32_t Filled = 0x9abcdef0u; + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(256) constexpr std::array FillCode{ + 0xd7460004, 0x04010c08, 0x7e000204, 0x7e020205, 0x7e040206, 0x7e060207, 0xe01c2000, 0x80000004, 0xe01c2000, 0x80020004, 0xbf810000, +}; + +std::uint64_t AddressOf(const void* data) { + return static_cast(reinterpret_cast(data)); +} + +std::array TextureDescriptor(const std::uint32_t* texels) { + const auto address = AddressOf(texels); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((Side - 1u) & 3u) << 30u), + ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), + 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +void Dispatch(std::vector& words, std::span code, std::uint32_t threads, std::uint32_t resource, std::span userData, std::uint32_t groups) { + const auto registers = [&](const std::uint32_t first, const std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{threads, 1, 1}; + registers(0x207, shape); + const auto address = AddressOf(code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array layout{resource}; + registers(0x213, layout); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); +} + +void Store(std::vector& words, const std::uint32_t* texels) { + std::array userData{}; + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + Dispatch(words, StoreCode, 8, static_cast(userData.size()) << 1u, userData, 1); +} + +void Fill(std::vector& words, const std::uint32_t* texels, const std::uint32_t* copied) { + const auto address = AddressOf(texels); + const auto other = AddressOf(copied); + const std::array userData{ + static_cast(address), + static_cast((address >> 32u) & 0xffffu) | (16u << 16u), + static_cast(SurfaceBytes / 16u), + 0x4bfacu, + Filled, + Filled, + Filled, + Filled, + static_cast(other), + static_cast((other >> 32u) & 0xffffu) | (16u << 16u), + static_cast(SurfaceBytes / 16u), + 0x4bfacu, + }; + Dispatch(words, FillCode, 64, (static_cast(userData.size()) << 1u) | 0x80u, userData, static_cast(SurfaceBytes / 1024u)); +} + +class Surface { +public: + Surface() { +#ifdef _WIN32 + region = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Block)); + if (region != nullptr) GuestArena::GuestArenaCommit_nid_postfix(region, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); +#else + void* mapped = mmap(nullptr, SurfaceBytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + Block - 1) & ~std::uintptr_t{Block - 1}; + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned != begin + Block) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + Block - aligned); + region = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(region, SurfaceBytes); + } +#endif + Require(region != nullptr, "direct write completion: cannot allocate the guest surface"); + texels = reinterpret_cast(region); + std::fill(texels, texels + SurfaceBytes / 4u, Untouched); + GuestAllocations::Mutation().Add(region, SurfaceBytes, true, true, true); + } + + ~Surface() { + GuestAllocations::Mutation().Remove(region); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(region, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(region, SurfaceBytes); +#else + munmap(region, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(region, SurfaceBytes); +#endif + } + + Surface(const Surface&) = delete; + Surface& operator=(const Surface&) = delete; + + std::uint32_t* Texels() const { return texels; } + +private: + std::uint8_t* region = nullptr; + std::uint32_t* texels = nullptr; +}; + +enum class Outcome { Kept, Dropped, Missed }; + +Outcome StoreAfterFill(const Surface& surface) { + std::vector copied(SurfaceBytes / 4u, Untouched); + GuestAllocations::Mutation().Add(copied.data(), SurfaceBytes, true, true, true); + struct Unregister { + void* data; + ~Unregister() { GuestAllocations::Mutation().Remove(data); } + } unregister{copied.data()}; + std::vector words; + Fill(words, surface.Texels(), copied.data()); + Store(words, surface.Texels()); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + std::vector landed(SurfaceBytes); + AgcDriver::GuestMemory::Read(AddressOf(copied.data()), landed); + const auto address = AddressOf(surface.Texels()); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex()); + const auto image = StorageTexture::FindPending(address, SurfaceBytes); + if (image == nullptr) return Outcome::Missed; + const bool current = image->Refresh(); + const bool pending = StorageTexture::FindPending(address, SurfaceBytes) == image; + StorageTexture::FlushPending(address, SurfaceBytes, nullptr, "direct write completion fixture"); + return current && pending ? Outcome::Kept : Outcome::Dropped; +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + { + Surface probe; + Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "direct write completion: unexpected surface size"); + } + { + constexpr std::size_t Runs = 16; + std::vector> surfaces; + for (std::size_t index = 0; index <= Runs; ++index) surfaces.push_back(std::make_unique()); + static_cast(StoreAfterFill(*surfaces[0])); + std::size_t kept = 0; + std::size_t dropped = 0; + for (std::size_t run = 1; run <= Runs; ++run) { + const auto outcome = StoreAfterFill(*surfaces[run]); + kept += outcome == Outcome::Kept; + dropped += outcome == Outcome::Dropped; + } + AgcDriverWaitIdle_nid_postfix(); + std::printf("[direct write completion] %zu of %zu runs held the image results pending, %zu dropped them\n", kept + dropped, Runs, dropped); + Require(kept + dropped != 0, "direct write completion: no run held the image results pending"); + Require(dropped == 0, "direct write completion: the completion of a buffer write dropped image results recorded after it"); + } + AgcDriverShutdown_nid_postfix(); + std::puts("direct write completion tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 2d1fa5aea512ac3809234f6e45ddeb62b3f16d42 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Tue, 20 Oct 2026 18:55:26 +0300 Subject: [PATCH 035/318] fix(libkernel): report O_RDWR from F_GETFL on both ends of a pipe FreeBSD 11's kern_pipe opens both ends with FREAD | FWRITE, so F_GETFL reports O_RDWR on each. The host pipe ends are one-way, so the access mode came back as O_RDONLY and O_WRONLY. Descriptors made by pipe() are now marked in the guest descriptor table, the mark follows dup, dup2 and F_DUPFD, and F_GETFL reports O_RDWR for them. --- .../File/include/DescriptorControl.hpp | 1 + .../libkernel/File/src/DescriptorControl.cpp | 19 ++++++++++++++++--- core/libs/prx/libkernel/File/src/Stdio.cpp | 4 ++-- core/libs/tests/GuestDup.cpp | 7 ++++--- core/libs/tests/GuestKernelFcntl.cpp | 12 +++++++----- docs/dev/debt/libkernel.md | 2 +- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 341324638c..1a76fc6de9 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -13,6 +13,7 @@ bool IoctlRequestMalformed(std::uint64_t request); int Ioctl(int fd, std::uint64_t request, void* argument); int DuplicateFrom(int fd, int start); void RememberDescriptor(int fd, int openFlags); +void RememberPipe(int fd); void ShareDescriptor(int from, int to); void ForgetDescriptor(int fd); bool Appends(int fd); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index a83f899801..9b29b05df3 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -114,6 +114,7 @@ enum class Kind { File, Directory, Pipe, Socket, Device, RandomDevice, ArchivedF struct Entry { bool closeOnExec = false; std::shared_ptr status; + bool pipe = false; }; std::mutex g_mutex; @@ -361,6 +362,12 @@ int CurrentStatus(int fd) { return *Materialize(fd).status; } +bool CreatedByPipe(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_entries.find(fd); + return found != g_entries.end() && found->second.pipe; +} + void SetCloseOnExec(int fd, bool value) { std::lock_guard lock(g_mutex); Materialize(fd).closeOnExec = value; @@ -481,7 +488,7 @@ int Fcntl(int fd, int command, std::intptr_t argument) { return 0; case GetStatusFlags: { if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); - const int access = AccessMode(fd, "fcntl"); + const int access = CreatedByPipe(fd) ? ReadWrite : AccessMode(fd, "fcntl"); return access | CurrentStatus(fd); } case SetStatusFlags: { @@ -560,11 +567,17 @@ void RememberDescriptor(int fd, int openFlags) { #endif } +void RememberPipe(int fd) { + RememberDescriptor(fd, 0); + std::lock_guard lock(g_mutex); + g_entries[fd].pipe = true; +} + void ShareDescriptor(int from, int to) { File::ShareArchivedFile(from, to); std::lock_guard lock(g_mutex); - auto status = Materialize(from).status; - g_entries[to] = Entry{false, std::move(status)}; + const auto& source = Materialize(from); + g_entries[to] = Entry{false, source.status, source.pipe}; #ifdef _WIN32 File::SharePositionedTransfer(from, to); GuestFileLockShare_nid_no_patch(from, to); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5e7f29b807..28e8e368cb 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -552,8 +552,8 @@ extern "C" int APS5_VABI pipe_nid_postfix(int* descriptors) { const int result = ::pipe(native); #endif if (result != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); - File::RememberDescriptor(native[0], 0); - File::RememberDescriptor(native[1], 0); + File::RememberPipe(native[0]); + File::RememberPipe(native[1]); std::memcpy(descriptors, native, sizeof(native)); return 0; } diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 4e0451ae85..0a8016a79b 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -190,6 +190,7 @@ static void DuplicatesShareStatusFlags() { constexpr int SetDescriptorFlags = 2; constexpr int GetFlags = 3; constexpr int SetFlags = 4; + constexpr int ReadWrite = 2; constexpr int NonBlocking = 4; constexpr int GuestEagain = 35; int pipe[2]; @@ -201,14 +202,14 @@ static void DuplicatesShareStatusFlags() { Require(duplicate >= 0); Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(duplicate, SetFlags, NonBlocking) == 0); - Require(fcntl_nid_postfix(pipe[0], GetFlags) == NonBlocking); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadWrite | NonBlocking)); Require(dup2_nid_postfix(pipe[0], other[0]) == other[0]); Require(fcntl_nid_postfix(other[0], GetDescriptorFlags) == 0); - Require(fcntl_nid_postfix(other[0], GetFlags) == NonBlocking); + Require(fcntl_nid_postfix(other[0], GetFlags) == (ReadWrite | NonBlocking)); char buffer[4] = {}; RequireFailure(static_cast(read_nid_postfix(other[0], buffer, sizeof(buffer))), GuestEagain); Require(fcntl_nid_postfix(pipe[0], SetFlags, 0) == 0); - Require(fcntl_nid_postfix(duplicate, GetFlags) == 0); + Require(fcntl_nid_postfix(duplicate, GetFlags) == ReadWrite); Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 1); for (const int descriptor : {pipe[0], pipe[1], other[0], other[1], duplicate}) Require(close_nid_postfix(descriptor) == 0); } diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index b75d3c22a9..3220eaa25c 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -425,8 +425,10 @@ static void RecordLocksBelongToTheProcess() { static void PipeFlagsRoundTrip() { int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); - Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); - Require(fcntl_nid_postfix(pipe[1], GetFlags) == WriteOnly); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(pipe[1], GetFlags) == ReadWrite); + const int duplicate = fcntl_nid_postfix(pipe[1], DuplicateCommand, 20); + Require(duplicate >= 20 && fcntl_nid_postfix(duplicate, GetFlags) == ReadWrite && close_nid_postfix(duplicate) == 0); Require(fcntl_nid_postfix(pipe[1], SetDescriptorFlags, 1) == 0); Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 1); Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 0); @@ -436,7 +438,7 @@ static void PipeFlagsRoundTrip() { Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(pipe[0], SetFlags, fcntl_nid_postfix(pipe[0], GetFlags) | NonBlocking) == 0); - Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadWrite | NonBlocking)); char buffer[8] = {}; RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); Require(write_nid_postfix(pipe[1], "abc", 3) == 3); @@ -448,10 +450,10 @@ static void PipeFlagsRoundTrip() { int off = 0; Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &off) == 0); - Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadWrite); int on = 1; Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &on) == 0); - Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadWrite | NonBlocking)); Require(fcntl_nid_postfix(pipe[1], SetFlags, NonBlocking) == 0); char block[1024] = {}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 446c7c554c..dfa8034c8c 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -202,7 +202,7 @@ - [sigaction](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` and `kill` accept every number from 1 to 128 now: a blocked one stays pending on the set of `sigtimedwait`, an unblocked one whose handler is registered runs it on the calling thread, and an unblocked one that has no host equivalent and no handler still fails with `EINVAL`, where FreeBSD runs its default action. `raise` of an `SA_SIGINFO` handler still throws instead of terminating - [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console -- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console +- [fcntl](../../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory throws. The return values and error codes are unverified on a console - [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured on a console. - [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [Regular file mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - on Linux `mmap` maps a regular file's descriptor shared or private like a shared memory object, with the same access checks; SQLite's WAL index (`-shm`) in Dusklight PPSA99305 needs it. FreeBSD 11 `mmap(2)` and shadPS4's `MemoryManager::MapFile` map regular files; the console's behaviour is not measured. Other descriptors (pipes, sockets, directories, devices) still fail with `EOPNOTSUPP`. On Windows a shared mapping is a view of a file section (`CreateFileMappingW` on the descriptor's handle, read-write when the descriptor is, otherwise read-only), so writes through the descriptor and the mapping see each other. This also maps anonymous `shm_open` objects and JIT shared memory descriptors, which are temporary files on Windows. A shared mapping may end inside the 16 KiB page that holds the end of the file, as POSIX [`mmap`](https://pubs.opengroup.org/onlinepubs/9799919799/functions/mmap.html) allows (llama.cpp in PS5LM Plus PPSA99581 maps a whole model file this way): the section keeps the file's size, so the file never grows, the view ends with the 4 KiB host page that holds the end of the file, which Windows fills with zeros past it, and the rest of the guest page is private zero pages. As POSIX requires, the bytes past the end read zero and are never written to the file; bytes written to those private pages are seen only through that mapping, not through other mappings of the file, and a file that grows later does not show its new bytes there. A shared mapping that reaches a whole page past the end of the file fails with `EOPNOTSUPP`, where POSIX maps it and delivers `SIGBUS` on access, as do `PROT_EXEC` and a write-only descriptor. A read-write alias (`GuestArenaMapAlias`) of a mapped page that holds the end of the file throws. `mprotect` above the view's protection (writing through a read-only descriptor, or adding `PROT_EXEC`) fails with `EACCES` before anything changes; FreeBSD allows `PROT_EXEC` on a readable descriptor. Shrinking the file with `ftruncate` fails with `EINVAL` while any shared mapping of it exists, even one that does not cover the removed part, because the section keeps the file's size at `mmap`; FreeBSD truncates. A read-write alias of a view (`GuestArenaMapAlias`, used when the GPU imports guest memory) of a read-only file section fails, as the alias is mapped `PAGE_READWRITE`. Views of the same file made by separate `mmap` calls are separate sections, so write tracking does not treat them as aliases of each other. A private mapping on Windows is a copy of the file read at `mmap`, which POSIX allows (later changes to the file need not show); pages wholly past the end of the file fail with `EOPNOTSUPP` instead of faulting on access, and a write-only descriptor fails with `EOPNOTSUPP`. From d3b2d56b205f5198a64b2aac1ce06e2618664d89 Mon Sep 17 00:00:00 2001 From: Longziip Date: Tue, 20 Oct 2026 17:32:20 +0100 Subject: [PATCH 036/318] fix(agc): collect CPU writes over kept storage image units before the completion stamps them StampKeepingNewerResults filtered the units it keeps with UnchangedSince, which reads only the block stamps. A CPU write over a pending unit that nothing had collected yet was absorbed into the completion's MarkWritten, and moving the unit to that generation kept older GPU texels over the CPU's bytes. Each candidate unit's range is now collected first, as the other pending-result decisions in Texture.cpp do. The direct write completion test now uses as many surfaces as fit in one settled host import block: a surface past it was imported alone below the settle floor and unwatched, so its record-time stamp returned 0 and its results were dropped by design. --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 5 ++++- .../tests/execution/DirectWriteCompletion.cpp | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index e548e78b1b..9f2d96de3b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -4058,7 +4058,10 @@ std::uint64_t StorageTexture::StampKeepingNewerResults(std::uint64_t address, st } std::erase_if(kept, [](const auto& entry) { const auto& [texture, unit] = entry; - return !GuestMemory::UnchangedSince(texture->layerBegin(unit), static_cast(texture->layerBytes(unit)), texture->layerGeneration[unit]); + const auto begin = texture->layerBegin(unit); + const auto unitBytes = static_cast(texture->layerBytes(unit)); + GuestMemory::CollectWritesUncached(begin, unitBytes); + return !GuestMemory::UnchangedSince(begin, unitBytes, texture->layerGeneration[unit]); }); const auto generation = stamp(); if (generation == 0 || kept.empty()) return generation; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp index 0a0d22a8bf..4035ab9f22 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp @@ -37,6 +37,7 @@ constexpr std::uint32_t TileR64KBX = 0x1b; constexpr std::uint32_t Type2D = 9; constexpr std::size_t Block = 0x10000; constexpr std::size_t SurfaceBytes = 4 * Block; +constexpr std::size_t SettledImportBytes = 64 * Block; constexpr std::uint32_t Untouched = 0xdeadbeefu; constexpr std::uint32_t Stored = 0x12345678u; constexpr std::uint32_t Filled = 0x9abcdef0u; @@ -198,7 +199,7 @@ int main() { Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(probe.Texels()))).guestBytes == SurfaceBytes, "direct write completion: unexpected surface size"); } { - constexpr std::size_t Runs = 16; + constexpr std::size_t Runs = SettledImportBytes / SurfaceBytes - 1; std::vector> surfaces; for (std::size_t index = 0; index <= Runs; ++index) surfaces.push_back(std::make_unique()); static_cast(StoreAfterFill(*surfaces[0])); From 021d6d98d658e13cd0ecdbadc791a2495a865ea4 Mon Sep 17 00:00:00 2001 From: Longziip Date: Tue, 20 Oct 2026 17:54:08 +0100 Subject: [PATCH 037/318] test(agc): drop storage image results over a CPU write made before the direct write completion The submission ends with a CPU-released WAIT_REG_MEM; the test writes the surface under GpuMutex once the image is pending and then releases the label, so the write lands after the record and before the completion's stamp. The image's results must be dropped and the CPU bytes kept. --- .../tests/execution/DirectWriteCompletion.cpp | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp index 4035ab9f22..889e5f46cb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DirectWriteCompletion.cpp @@ -17,6 +17,8 @@ #endif #include #include +#include +#include #include #include #include @@ -25,6 +27,7 @@ #include #include #include +#include #include namespace { @@ -41,6 +44,7 @@ constexpr std::size_t SettledImportBytes = 64 * Block; constexpr std::uint32_t Untouched = 0xdeadbeefu; constexpr std::uint32_t Stored = 0x12345678u; constexpr std::uint32_t Filled = 0x9abcdef0u; +constexpr std::uint32_t Written = 0x0badf00du; alignas(256) constexpr std::array StoreCode{ 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, @@ -158,7 +162,23 @@ private: enum class Outcome { Kept, Dropped, Missed }; -Outcome StoreAfterFill(const Surface& surface) { +void WriteOnceRecorded(const Surface& surface) { + const auto address = AddressOf(surface.Texels()); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + for (;;) { + { + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex()); + if (StorageTexture::FindPending(address, SurfaceBytes) != nullptr) { + surface.Texels()[0] = Written; + return; + } + } + Require(std::chrono::steady_clock::now() < deadline, "direct write completion: the image store was never recorded"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +Outcome StoreAfterFill(const Surface& surface, bool cpuWrite) { std::vector copied(SurfaceBytes / 4u, Untouched); GuestAllocations::Mutation().Add(copied.data(), SurfaceBytes, true, true, true); struct Unregister { @@ -168,8 +188,18 @@ Outcome StoreAfterFill(const Surface& surface) { std::vector words; Fill(words, surface.Texels(), copied.data()); Store(words, surface.Texels()); + alignas(8) static std::uint32_t gate = 0; + gate = 0; + if (cpuWrite) { + const auto at = AddressOf(&gate); + words.insert(words.end(), {0xc0053c00u, 0x13u, static_cast(at), static_cast(at >> 32u), 1u, 0xffffffffu, 0x19u}); + } Packet packet{words.data(), static_cast(words.size()), 0, {}}; AgcDriver::Submit(&packet, 0); + if (cpuWrite) { + WriteOnceRecorded(surface); + std::atomic_ref(gate).store(1); + } AgcDriverWaitIdle_nid_postfix(); std::vector landed(SurfaceBytes); AgcDriver::GuestMemory::Read(AddressOf(copied.data()), landed); @@ -201,12 +231,12 @@ int main() { { constexpr std::size_t Runs = SettledImportBytes / SurfaceBytes - 1; std::vector> surfaces; - for (std::size_t index = 0; index <= Runs; ++index) surfaces.push_back(std::make_unique()); - static_cast(StoreAfterFill(*surfaces[0])); + for (std::size_t index = 0; index <= 2 * Runs + 1; ++index) surfaces.push_back(std::make_unique()); + static_cast(StoreAfterFill(*surfaces[0], false)); std::size_t kept = 0; std::size_t dropped = 0; for (std::size_t run = 1; run <= Runs; ++run) { - const auto outcome = StoreAfterFill(*surfaces[run]); + const auto outcome = StoreAfterFill(*surfaces[run], false); kept += outcome == Outcome::Kept; dropped += outcome == Outcome::Dropped; } @@ -214,6 +244,17 @@ int main() { std::printf("[direct write completion] %zu of %zu runs held the image results pending, %zu dropped them\n", kept + dropped, Runs, dropped); Require(kept + dropped != 0, "direct write completion: no run held the image results pending"); Require(dropped == 0, "direct write completion: the completion of a buffer write dropped image results recorded after it"); + static_cast(StoreAfterFill(*surfaces[Runs + 1], false)); + std::size_t keptOverWrite = 0; + std::size_t lostWrites = 0; + for (std::size_t run = Runs + 2; run <= 2 * Runs + 1; ++run) { + keptOverWrite += StoreAfterFill(*surfaces[run], true) == Outcome::Kept; + lostWrites += surfaces[run]->Texels()[0] != Written; + } + AgcDriverWaitIdle_nid_postfix(); + std::printf("[direct write completion] %zu of %zu runs with a CPU write before the completion kept the image results, %zu lost the write\n", keptOverWrite, Runs, lostWrites); + Require(keptOverWrite == 0, "direct write completion: image results were kept over a CPU write made before the completion"); + Require(lostWrites == 0, "direct write completion: a CPU write made before the completion was lost"); } AgcDriverShutdown_nid_postfix(); std::puts("direct write completion tests passed"); From d046ddee8c607f2d1e6ecde295407e201c56a769 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:07:13 +0100 Subject: [PATCH 038/318] fix(agc): zero declined DCC clear fills and partial trailing elements --- .../Graphics/src/DccMetadata.cpp | 9 +++++++-- .../prx/libSceAgcDriver/tests/TextureFormat.cpp | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 043bb2697a..709eae6300 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -652,7 +652,10 @@ bool FillDccClear(VkFormat format, DccKeys keys, bool alphaOnMsb, std::span(layout.channels) - 1 : 0; @@ -667,7 +670,9 @@ bool FillDccClear(VkFormat format, DccKeys keys, bool alphaOnMsb, std::span> i) & 1u) != 0) element[bit / 8u] |= static_cast(1u << (bit % 8u)); } } - for (std::size_t offset = 0; offset + element.size() <= bytes.size(); offset += element.size()) std::memcpy(bytes.data() + offset, element.data(), element.size()); + std::size_t offset = 0; + for (; offset + element.size() <= bytes.size(); offset += element.size()) std::memcpy(bytes.data() + offset, element.data(), element.size()); + std::fill(bytes.begin() + offset, bytes.end(), std::byte{0}); return true; } diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index bf25b268ee..85fc02afb3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -204,6 +204,22 @@ void packedFloatDccClearTests() { Require(TextureClearKeys(resource, 1024) == DccKeys::Clear1111, "packed float texture discarded its white fast-clear keys"); } +void fillContractTests() { + std::array texels; + texels.fill(0xdeadbeefu); + auto span = std::as_writable_bytes(std::span(texels)); + Require(!FillDccClear(VK_FORMAT_R8G8B8A8_UNORM, DccKeys::ClearRegister, false, span), "a register clear encoded a texel"); + for (const auto texel : texels) Require(texel == 0u, "a declined clear left its span untouched"); + texels.fill(0xdeadbeefu); + Require(!FillDccClear(VK_FORMAT_R8G8B8A8_UNORM, DccKeys::Uncompressed, false, span), "an uncompressed code encoded a texel"); + for (const auto texel : texels) Require(texel == 0u, "a declined clear left its span untouched"); + std::array partial; + partial.fill(std::byte{0x5a}); + Require(FillDccClear(VK_FORMAT_B10G11R11_UFLOAT_PACK32, DccKeys::Clear1111, false, partial), "packed float fast clear was not encoded"); + const std::array white{std::byte{0xc0}, std::byte{0x03}, std::byte{0x1e}, std::byte{0x78}}; + for (std::size_t index = 0; index < partial.size(); ++index) Require(partial[index] == (index < 8u ? white[index % 4u] : std::byte{0}), "a clear fill over a partial trailing element did not zero the tail"); +} + } void RunTextureFormatTests() { @@ -266,4 +282,5 @@ void RunTextureFormatTests() { convertedDccClearTests(); singleDccKeysTests(); packedFloatDccClearTests(); + fillContractTests(); } From 667f3c5b4d910656161f7182934a3c62d14731b2 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:07:13 +0100 Subject: [PATCH 039/318] perf(agc): age-bound carried snapshot buffers in the sampled cache --- .../libSceAgcDriver/Graphics/src/ShaderResources.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 6e9632f272..d3abcdffd8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -120,8 +120,11 @@ struct CachedTexture { // write watch; zero when none was taken. std::array untracked{}; std::vector carried; + std::uint64_t carriedPresent = 0; }; +constexpr std::uint64_t CarriedBufferMaxPresents = 16; + // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the // eviction takes the back. APS5_NO_TEXTURE_HASH=1 finds entries by scanning the list (the index is // still kept), the linear lookup of before. @@ -776,6 +779,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span recycled; if (auto it = findTexture(cache, key); it != cache.entries.end()) { if (it->source != nullptr) { + if (!it->carried.empty() && Recorder::Presents() - it->carriedPresent > CarriedBufferMaxPresents) it->carried = std::vector{}; // The view follows the storage image, whatever the GPU wrote to it since; guest memory // written meanwhile is taken in by refreshing the image. A fast clear the image cannot // see (keys, and no pending results to prefer) ends the view: a snapshot holds the clear. @@ -820,12 +824,15 @@ std::shared_ptr cachedTexture(const Context& context, std::spanbytes.size() == bytes) recycled = std::move(it->bytes); - else if (it->carried.size() == bytes) recycled = std::move(it->carried); + else if (it->carried.size() == bytes && Recorder::Presents() - it->carriedPresent <= CarriedBufferMaxPresents) recycled = std::move(it->carried); eraseTexture(cache, it); ++cache.counts.stale; } CachedTexture entry{key, address, source != nullptr ? std::vector{} : (recycled.empty() ? std::vector(bytes) : std::move(recycled)), nullptr, *keys, snapshotGeneration}; - if (source != nullptr) entry.carried = std::move(recycled); + if (source != nullptr) { + entry.carried = std::move(recycled); + entry.carriedPresent = Recorder::Presents(); + } entry.committed = std::move(committed); if (source != nullptr) { entry.source = source; From 66e8ffd7e683d910913e0f12498900846e5e388a Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:07:13 +0100 Subject: [PATCH 040/318] test(agc): cover sampled snapshot buffer recycling --- .../tests/execution/SampledCacheBudget.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 881867d747..9b6a76ecba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -443,6 +443,22 @@ void snapshotTests(const Context& context) { Require(TextureCacheUsage().sampledBytes == tiny->AllocationBytes(), "a snapshot whose " + std::to_string(tiny->AllocationBytes()) + "-byte image is smaller than its " + std::to_string(guestBytes) + " guest bytes counted " + std::to_string(TextureCacheUsage().sampledBytes) + " bytes, not its allocation"); } +void recycleTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + const auto address = block.Surface(0); + std::memset(reinterpret_cast(address), 0xa1, SurfaceBytes); + const auto first = Sampled(context, address); + Require(Sampled(context, address) == first, "an unchanged surface did not serve its snapshot"); + std::memset(reinterpret_cast(address), 0xb2, SurfaceBytes); + const auto second = Sampled(context, address); + Require(second != first, "a changed surface did not replace its snapshot"); + std::memset(reinterpret_cast(address), 0xa1, SurfaceBytes); + const auto third = Sampled(context, address); + Require(third != second, "the replaced snapshot's size-equal buffer was not reread from guest memory"); + Require(Sampled(context, address) == third, "an unchanged surface did not serve its remade snapshot"); +} + void heldTests(const Context& context) { Block block(context); auto held = Sampled(context, block.Surface(0)); @@ -769,6 +785,7 @@ int main(int argc, char** argv) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); reportedBudgetTests(context); snapshotTests(context); + recycleTests(context); heldTests(context); releasedTests(context); rescanTests(context); From aa210e4e5adfa6302869a603f1c589bec8cc8866 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:00:36 +0200 Subject: [PATCH 041/318] feat(agc): render 4_4_4_4 UNORM color targets through packed R16_UINT exports CB_COLOR_INFO format 19 with UNORM and COMP_SWAP 2 lists A, B, G, R from the least significant bits. No Vulkan device is required to render into the 4-bit formats and NVIDIA exposes none of them as color attachments, so the target is an R16_UINT image and the pixel shader packs the exports into the guest layout, as the 10_11_11 target does. Writes of fewer than four components, blending, DCC and truncating rounding are refused. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 10 ++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 45 ++++++++++++++++++- .../ControlFlow/src/RequestSerializer.cpp | 2 +- .../FragmentOutputSpecialization.hpp | 2 +- core/shader/recompiler/Recompiler.hpp | 3 +- .../SpirvBackend/src/SpirvBufferFormat.cpp | 3 ++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 4 +- 7 files changed, 62 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 090a68abc1..89a9db0042 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -807,6 +807,7 @@ struct DecodedColorFormat { }; const char* PackedColorTargetName(ShaderRecompiler::ColorExportPacking packing) { + if (packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4) return "4_4_4_4 unorm"; return packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "11_11_10 float" : "10_11_11 unorm"; } @@ -820,7 +821,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, }; const bool alternate = swap == 1; const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast((0xe4u & ~3u) | swap)}; }; - if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 7 && format != 10 && format != 12 && format != 16) return fail(); + if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 7 && format != 10 && format != 12 && format != 16 && format != 19) return fail(); switch (format) { case 1: if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1); @@ -898,6 +899,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, case 17: if (swap != 0 || number != unorm) return fail(); return {VK_FORMAT_A1R5G5B5_UNORM_PACK16, 2, 0xc6u}; + case 19: + if (swap != 2 || number != unorm) return fail(); + return {VK_FORMAT_R16_UINT, 2, 0x1bu, ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4}; default: return fail(); } @@ -1461,7 +1465,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu; if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into " + packed + " target is unsupported"); Require(!state.blendEnable, "blending into " + packed + " color target is unsupported"); - Require((exportedMask & 7u) == 7u, "partial writes of " + packed + " color target are unsupported"); + const std::uint32_t packedMask = color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 ? 0xfu : 7u; + Require((exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; }; @@ -1721,6 +1726,7 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo color.packing = decoded.packing; if (color.packing != ShaderRecompiler::ColorExportPacking::None && (info & 0x10000000u) != 0) throw std::runtime_error("AGC graphics: DCC-compressed " + std::string(PackedColorTargetName(color.packing)) + " color targets are unsupported"); Require(color.packing != ShaderRecompiler::ColorExportPacking::Unorm10_11_11 || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 10_11_11 unorm color targets are unsupported"); + Require(color.packing != ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 4_4_4_4 unorm color targets are unsupported"); Require(color.packing != ShaderRecompiler::ColorExportPacking::Float11_11_10 || (info & 0x00040000u) != 0, "rounding (ROUND_MODE 0) 11_11_10 float color targets are unsupported"); for (std::uint32_t word = 0; word < 2; ++word) { const auto clear = find(cx, 0x323 + word + stride); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index e28a78b241..79c1f4a4ef 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8793,6 +8793,48 @@ void unorm10_11_11TargetTests() { expectFailure([&] { static_cast(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no color export packing path"); } +void unorm4_4_4_4TargetTests() { + using ShaderRecompiler::ColorExportPacking; + auto queue = makeState(); + queue.context[0x31c] = (queue.context.at(0x31c) & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (19u << 2u) | (2u << 11u); + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R16_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0x1bu && state.color.packing == ColorExportPacking::Unorm4_4_4_4, "a 4_4_4_4 unorm target did not decode to an R16_UINT attachment with reversed exports"); + const auto packings = AgcDriver::Graphics::ExportPackings(state); + Require(packings[0] == ColorExportPacking::Unorm4_4_4_4 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 4_4_4_4 unorm packing"); + auto changed = queue; + changed.context[0x8e] = 7; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a 4_4_4_4 unorm color target"); + for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { + changed = queue; + changed.context[0x31c] = (changed.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 19 number type " + std::to_string(number) + " component swap " + std::to_string(swap)); + } + changed = queue; + changed.context[0x31c] |= 0x10000000u; + changed.context[0x325] = 0x1234; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "DCC-compressed 4_4_4_4 unorm color targets"); + changed = queue; + changed.context[0x31c] |= 0x40000u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "truncating (ROUND_MODE) 4_4_4_4 unorm color targets"); + + constexpr std::array code{0xf800180fu, 0x07060504u, 0xbf810000u}; + const auto request = packedExportRequest(queue, state, code); + auto plain = request; + plain.context.pixel->targetExportPacking.fill(ColorExportPacking::None); + const ShaderRecompiler::RequestSerializer serializer; + Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->targetExportPacking == request.context.pixel->targetExportPacking, "the 4_4_4_4 unorm packing was lost in serialization"); + const auto packed = ShaderRecompiler::Recompile(request); + const auto unpacked = ShaderRecompiler::Recompile(plain); + Require(packed.PipelineVariantId() != unpacked.PipelineVariantId() && !(packed.spirv == unpacked.spirv), "the 4_4_4_4 export packing did not specialize the module"); + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &packed, 0}}}; + Require(AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set{0u}, "the 4_4_4_4 packed export did not reach attachment 0"); +} + void float11_11_10TargetTests() { using ShaderRecompiler::ColorExportPacking; auto queue = makeState(); @@ -8854,7 +8896,7 @@ void float11_11_10TargetTests() { const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &packed, 0}}}; Require(AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set{0u}, "the packed float export did not reach attachment 0"); auto invalid = request; - invalid.context.pixel->targetExportPacking[0] = static_cast(3); + invalid.context.pixel->targetExportPacking[0] = static_cast(4); expectFailure([&] { static_cast(ShaderRecompiler::Recompile(invalid)); }, "invalid prepared fragment export packing"); } @@ -10383,6 +10425,7 @@ int main(int argc, char** argv) { ZExportTests(); unorm10_11_11TargetTests(); float11_11_10TargetTests(); + unorm4_4_4_4TargetTests(); DepthBoundsBiasTests(); conservativeZExportTests(); earlyFragmentTestsTests(); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 76e0e11888..34d40e5e35 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -239,7 +239,7 @@ ConservativeZExport readConservativeZExport(Reader& reader) { ColorExportPacking readColorExportPacking(Reader& reader) { const auto value = reader.ReadU8(); - if (value > static_cast(ColorExportPacking::Float11_11_10)) { + if (value > static_cast(ColorExportPacking::Unorm4_4_4_4)) { throw std::runtime_error("RequestSerializer: invalid ColorExportPacking value"); } return static_cast(value); diff --git a/core/shader/recompiler/FragmentOutputSpecialization.hpp b/core/shader/recompiler/FragmentOutputSpecialization.hpp index 00894c6af1..e3360cceaf 100644 --- a/core/shader/recompiler/FragmentOutputSpecialization.hpp +++ b/core/shader/recompiler/FragmentOutputSpecialization.hpp @@ -32,7 +32,7 @@ inline std::vector SpecializeFragmentOutputs(std::vector= PipelineSpecialization::ExportPackingBase + 8u) continue; - if (constant.value > static_cast(ColorExportPacking::Float11_11_10)) throw std::runtime_error("invalid prepared fragment export packing"); + if (constant.value > static_cast(ColorExportPacking::Unorm4_4_4_4)) throw std::runtime_error("invalid prepared fragment export packing"); if (constant.value != static_cast(ColorExportPacking::None)) packed.insert(constant.id - PipelineSpecialization::ExportPackingBase); } if (packed.empty() && !dualSource && reversed == 0u) return words; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 9ca828bf3d..b167b6c7f1 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -113,7 +113,8 @@ enum class ConservativeZExport : std::uint8_t { enum class ColorExportPacking : std::uint8_t { None, Unorm10_11_11, - Float11_11_10 + Float11_11_10, + Unorm4_4_4_4 }; enum class RasterPrimitive : std::uint8_t { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp index 76ecba7a65..c2ee940102 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBufferFormat.cpp @@ -55,6 +55,7 @@ SpirvFormatComponentType GetFormatComponentType(IrBufferFormat format) { case IrBufferFormat::Format10_10_10_2UNorm: case IrBufferFormat::Format8_8_8_8UNorm: case IrBufferFormat::Format16_16_16_16UNorm: + case IrBufferFormat::Format4_4_4_4UNorm: return SpirvFormatComponentType::Unorm; case IrBufferFormat::Format8SNorm: case IrBufferFormat::Format16SNorm: @@ -110,6 +111,8 @@ SpirvBufferFormatInfo GetFormatInfo(IrBufferFormat format) { } const auto type = GetFormatComponentType(format); switch (format) { + case IrBufferFormat::Format4_4_4_4UNorm: + return {format, type, 4u, 2u, {4u, 4u, 4u, 4u}, {0u, 4u, 8u, 12u}, true}; case IrBufferFormat::Format8UNorm: case IrBufferFormat::Format8SNorm: case IrBufferFormat::Format8UScaled: diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index e5fbb7f67c..49cbac0bcf 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -400,7 +400,9 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target const auto packing = state.module.SpecializationConstant(TypeU32(state), PipelineSpecialization::ExportPackingBase + target, static_cast(ColorExportPacking::None)); const auto packed = EmitCompareU32Constant(state, spv::OpINotEqual, packing, static_cast(ColorExportPacking::None)); const auto floating = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Float11_11_10)); - const auto word = Select(state, TypeU32(state), floating, PackExport(state, IrBufferFormat::Format10_11_11Float, color), PackExport(state, IrBufferFormat::Format11_11_10UNorm, color)); + const auto small = EmitCompareU32Constant(state, spv::OpIEqual, packing, static_cast(ColorExportPacking::Unorm4_4_4_4)); + const auto wide = Select(state, TypeU32(state), floating, PackExport(state, IrBufferFormat::Format10_11_11Float, color), PackExport(state, IrBufferFormat::Format11_11_10UNorm, color)); + const auto word = Select(state, TypeU32(state), small, PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color), wide); std::array components{}; for (std::uint32_t component = 0; component < components.size(); ++component) { const auto unpacked = state.module.AllocateId(); From 48a7efcbec9b6116acda84fcbe90405d8190003a Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:07:11 +0200 Subject: [PATCH 042/318] feat(agc): merge partial writes into 4_4_4_4 UNORM color targets A CB_TARGET_MASK that writes fewer than four components of a 4_4_4_4 target leaves the other nibbles of each packed word as they were, which an R16_UINT attachment cannot do. Such draws now close the attachment's color write mask, and the pixel shader merges the written nibbles into the target through a storage image: inside a pixel interlock it reads the word, keeps the bits the mask leaves alone and stores it back. The merge (export index, word mask, sample count) is part of the static shader key, request version 40 and shader disk cache format 42; the storage image is appended to the shader's images with a single runtime mode, and its T# is encoded from the target's surface at draw time, so the draw resolves it to the target's resident image, which Draw requires. Merging shaders declare early fragment tests; shaders that kill under late tests, export depth, stencil or coverage, use alpha-to-coverage or a quad sample mask are refused, as are multisampled, mipmapped, volume and array targets and more than one merged target per draw. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Execution/src/Driver/Draw/DrawDecode.cpp | 1 + .../Execution/src/Driver/Draw/DrawLookup.cpp | 4 + .../src/Driver/Draw/DrawRegisterKey.cpp | 4 +- .../Graphics/include/GuestTextureResource.hpp | 1 + .../Graphics/include/State.hpp | 3 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 + .../Graphics/src/GuestTextureResource.cpp | 34 ++ .../Graphics/src/PipelineJournal.cpp | 2 +- .../libSceAgcDriver/Graphics/src/State.cpp | 40 ++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 77 ++++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 46 ++- .../execution/PackedUnorm4444PartialWrite.cpp | 324 ++++++++++++++++++ core/shader/recompiler/CacheKey.hpp | 3 + .../ControlFlow/src/RequestSerializer.cpp | 16 +- .../IrMetadata/Resources.hpp | 1 + .../Optimization/ShaderStageInputInfo.hpp | 3 + .../Optimization/src/ResourceMaterializer.cpp | 1 + .../Optimization/src/ShaderInfoCollector.cpp | 20 ++ core/shader/recompiler/Recompiler.cpp | 24 +- core/shader/recompiler/Recompiler.hpp | 10 + core/shader/recompiler/ShaderDiskCache.cpp | 4 +- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../SpirvMemory/SpirvDescriptors.hpp | 1 + .../src/SpirvMemory/SpirvDescriptors.cpp | 16 + .../src/SpirvMemory/SpirvInputOutput.cpp | 2 +- .../src/SpirvMemory/SpirvModuleSetup.cpp | 10 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 26 ++ .../src/ShaderInputInfoBuilder.cpp | 4 + .../tests/RequestSerializerTests.cpp | 20 +- 31 files changed, 670 insertions(+), 38 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 79e28aa9b6..e37e75bd3a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1956,6 +1956,8 @@ target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $< target_include_directories(agc_driver_packed_unorm4444_color_target_tests PRIVATE $) +target_include_directories(agc_driver_packed_unorm4444_partial_write_tests PRIVATE $) + target_include_directories(agc_driver_color1555_target_tests PRIVATE $) target_include_directories(agc_driver_color565_target_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index f17c3ce9df..915861e8d3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -165,6 +165,7 @@ std::shared_ptr Driver::decodeDraw(const QueueState& queue, const Su product->pixel.targetExportPacking = Graphics::ExportPackings(product->state); product->pixel.dualSourceBlend = product->state.dualSourceBlend; product->pixel.reversedBlend = product->state.reversedBlend; + product->pixel.targetMerge = Graphics::ExportTargetMerge(product->state); product->pixel.alphaToCoverage = product->state.alphaToCoverage; product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state); product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index b0ae591522..6cbf22ec75 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -74,6 +74,10 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(value)); mix(pixel.dualSourceBlend); mix(pixel.reversedBlend); + mix(pixel.targetMerge.target); + mix(pixel.targetMerge.mask); + mix(pixel.targetMerge.samples); + for (const auto word : pixel.targetMerge.descriptor) mix(word); mix(pixel.alphaToCoverage); mix(static_cast(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 129e751733..c0e563f02e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -228,7 +228,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto& s = a.state; const auto& t = b.state; const auto sameColor = [](const Graphics::ColorTarget& x, const Graphics::ColorTarget& y) { - return x.address == y.address && x.extent.width == y.extent.width && x.extent.height == y.extent.height && x.format == y.format && x.bytes == y.bytes && x.componentMapping == y.componentMapping && x.tileMode == y.tileMode && x.elementBytes == y.elementBytes && x.dccAddress == y.dccAddress && x.dccAlphaOnMsb == y.dccAlphaOnMsb && x.slot == y.slot && x.exportIndex == y.exportIndex; + return x.address == y.address && x.extent.width == y.extent.width && x.extent.height == y.extent.height && x.format == y.format && x.bytes == y.bytes && x.componentMapping == y.componentMapping && x.tileMode == y.tileMode && x.elementBytes == y.elementBytes && x.dccAddress == y.dccAddress && x.dccAlphaOnMsb == y.dccAlphaOnMsb && x.slot == y.slot && x.exportIndex == y.exportIndex && x.partialWriteMask == y.partialWriteMask; }; const auto sameBlend = [](const VkPipelineColorBlendAttachmentState& x, const VkPipelineColorBlendAttachmentState& y) { return x.blendEnable == y.blendEnable && x.srcColorBlendFactor == y.srcColorBlendFactor && x.dstColorBlendFactor == y.dstColorBlendFactor && x.colorBlendOp == y.colorBlendOp && x.srcAlphaBlendFactor == y.srcAlphaBlendFactor && x.dstAlphaBlendFactor == y.dstAlphaBlendFactor && x.alphaBlendOp == y.alphaBlendOp && x.colorWriteMask == y.colorWriteMask; @@ -262,7 +262,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { const auto& y = b.programs[i]; if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } - if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false; + if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.targetMerge != q.targetMerge || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false; if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp index 6f40129b56..5725bf61d3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp @@ -96,6 +96,7 @@ float EffectiveMinLod(const GuestTextureResource& resource); bool IsNullTextureResource(std::span words); GuestTextureResource DecodeTextureResource(std::span words); +std::array EncodeTextureResource(const GuestTextureResource& resource); bool DescriptorSingleLevel(std::span words); bool FirstLayerDescriptor(std::span words, std::array& narrowed); bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 22eb13371f..dbd451d6a6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -68,6 +68,7 @@ struct ColorTarget { std::uint32_t pipeBankXor = 0; std::uint32_t samples = 1; std::uint32_t dccControl = 0; + std::uint16_t partialWriteMask = 0; }; struct DepthTarget { @@ -169,6 +170,8 @@ std::array PixelSampleLocations(const State& state, const Shad VkExtent2D SampleLocationGrid(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); +std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask); +ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); bool PartialSampleMask(const State& state); bool UniformSampleMask(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index e0a69a53ac..3c3115e9b5 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2433,6 +2433,12 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& for (const auto& binding : targets) { if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); } + for (const auto& binding : targets) { + if (binding.color.partialWriteMask == 0) continue; + Require(lean && binding.resident != nullptr && !binding.proxied, "a partial write of a packed color target is merged into its resident image by a recorded draw only"); + const auto images = resources->StorageImages(); + Require(std::ranges::any_of(images, [&](const std::pair& image) { return image.first == binding.resident->Image() && image.second; }), "the partial write of a packed color target does not store into the target's resident image"); + } if (!lean && !resolved.moved.empty()) { resolved.resources = std::make_shared(context, shaders, state.color, static_cast(state.colors.size()), draw.indexAddress, static_cast(indexBytes), snapshots); resolved.moved.clear(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 3c81c9c97e..1acb77e3d9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -242,6 +242,40 @@ GuestTextureResource DecodeTextureResource(std::span words) return result; } +std::array EncodeTextureResource(const GuestTextureResource& resource) { + Require(resource.dimension == TextureDimension::k2D && resource.samples == 1u, "only single-sample 2D surfaces have a texture descriptor encoding"); + Require(resource.mipCount == 1u && resource.allocatedMipCount <= 1u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.depthOrLastArray == 0u && resource.baseArray == 0u && resource.dccAddress == 0u && resource.minLod == 0u && !resource.iterate256 && resource.bcSwizzle == 0u, "only single-level, single-layer surfaces without metadata have a texture descriptor encoding"); + Require(resource.width >= 1u && resource.width <= 0x4000u && resource.height >= 1u && resource.height <= 0x4000u && resource.format < 0x200u, "the surface's extent or format has no texture descriptor encoding"); + const auto xorMask = resource.tileMode == TextureTileMode::kS4KBX || resource.tileMode == TextureTileMode::kD4KBX ? 0xfffull : 0xffffull; + Require(XorSwizzleMode(resource.tileMode) != 0 ? (resource.baseAddress & xorMask) == 0 && (resource.pipeBankXor & ~xorMask) == 0 : resource.pipeBankXor == 0, "the surface's pipe and bank XOR has no texture descriptor encoding"); + const auto base = (resource.baseAddress | resource.pipeBankXor) >> 8u; + Require(base != 0 && (resource.baseAddress & 0xffu) == 0 && base <= 0xffffffffffull, "the surface's address has no texture descriptor encoding"); + std::uint32_t tileMode = 0; + switch (resource.tileMode) { + case TextureTileMode::kLinear: tileMode = 0x00; break; + case TextureTileMode::kStandard256B: tileMode = 0x01; break; + case TextureTileMode::kD256B: tileMode = 0x02; break; + case TextureTileMode::kStandard4KB: tileMode = 0x05; break; + case TextureTileMode::kD4KB: tileMode = 0x06; break; + case TextureTileMode::kStandard64KB: tileMode = 0x09; break; + case TextureTileMode::kD64KB: tileMode = 0x0a; break; + case TextureTileMode::kS64KBT: tileMode = 0x11; break; + case TextureTileMode::kD64KBT: tileMode = 0x12; break; + case TextureTileMode::kS4KBX: tileMode = 0x15; break; + case TextureTileMode::kD4KBX: tileMode = 0x16; break; + case TextureTileMode::kZ64KBX: tileMode = 0x18; break; + case TextureTileMode::kS64KBX: tileMode = 0x19; break; + case TextureTileMode::kD64KBX: tileMode = 0x1a; break; + case TextureTileMode::kR64KBX: tileMode = 0x1b; break; + } + std::array words{}; + words[0] = static_cast(base); + words[1] = static_cast(base >> 32u) | (resource.format << 20u) | (((resource.width - 1u) & 3u) << 30u); + words[2] = ((resource.width - 1u) >> 2u) | ((resource.height - 1u) << 14u); + words[3] = resource.dstSelX | (resource.dstSelY << 3u) | (resource.dstSelZ << 6u) | (resource.dstSelW << 9u) | (tileMode << 20u) | (9u << 28u); + return words; +} + float EffectiveMinLod(const GuestTextureResource& resource) { if (resource.minLod <= resource.baseLevel * 256u) return 0.0f; return std::min(static_cast(resource.minLod) / 256.0f, static_cast(resource.lastLevel)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index c7a0f679a5..886a63f60b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -35,7 +35,7 @@ static_assert(sizeof(VkStencilOpState) == 28, "VkStencilOpState changed: update static_assert(sizeof(VkVertexInputBindingDescription) == 12, "VkVertexInputBindingDescription changed: update the recipe codec"); static_assert(sizeof(VkVertexInputAttributeDescription) == 16, "VkVertexInputAttributeDescription changed: update the recipe codec"); #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) -static_assert(sizeof(State) == 784, "State changed: update CanonicalPipelineState and the recipe codec"); +static_assert(sizeof(State) == 792, "State changed: update CanonicalPipelineState and the recipe codec"); static_assert(sizeof(ShaderStages) == 112, "ShaderStages changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::MeshConfiguration) == 40, "MeshConfiguration changed: update the recipe codec"); static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "TessellationConfiguration changed: update the recipe codec"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 89a9db0042..c6368dc363 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1397,7 +1397,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { result.blends.assign(exportCount, VkPipelineColorBlendAttachmentState{}); std::array requiredBlendConstants{}; std::uint32_t requiredBlendMask = 0; - for (const auto& color : result.colors) { + for (auto& color : result.colors) { const auto slot = color.slot; const auto blend = read(cx, 0x1e0 + slot); APS5_LOG_OUT_DEBUG("Blend %u control=0x%x", slot, blend); @@ -1465,8 +1465,16 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu; if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into " + packed + " target is unsupported"); Require(!state.blendEnable, "blending into " + packed + " color target is unsupported"); - const std::uint32_t packedMask = color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 ? 0xfu : 7u; - Require((exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); + const bool nibbles = color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4; + const std::uint32_t packedMask = nibbles ? 0xfu : 7u; + Require(nibbles || (exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); + if ((exportedMask & packedMask) != packedMask) { + Require(color.samples == 1u, "partial writes of a multisampled " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented"); + Require(color.mipCount == 1u && color.depth == 1u && color.layers == 1u && color.arrayLastSlice == 0u, "partial writes of a mipmapped, volume or array " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented"); + Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "partial writes of more than one packed color target are not implemented"); + color.partialWriteMask = PackedWriteMask(mapping, exportedMask); + state.colorWriteMask = 0; + } } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; }; @@ -1483,7 +1491,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { for (std::uint32_t i = 0; i < 3; ++i) { if ((requiredBlendMask & (1u << i)) != 0) result.blendConstants[i] = requiredBlendConstants[i]; } - if (!result.colors.empty()) result.blend = result.blends[result.colors.front().exportIndex]; + if (!result.colors.empty()) { + result.color = result.colors.front(); + result.blend = result.blends[result.colors.front().exportIndex]; + } Require(result.reversedBlend == 0 || (exportSlots.size() == 1 && exportCount == 1), "blending into a color target with a reversed component order through a second source color needs that target to be the shader's only color export"); if (result.dualSourceBlend) { Require(exportSlots.size() == 2 && exportSlots[0] == 0 && exportSlots[1] == 1 && exportCount == 1, "dual-source blending needs pixel exports to MRT slots 0 and 1 and color writes to slot 0 alone"); @@ -1510,6 +1521,27 @@ std::array ExportPackings(const State& return packings; } +std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask) { + std::uint16_t mask = 0; + for (std::uint32_t component = 0; component < 4; ++component) { + if (((exportedMask >> ((componentMapping >> (2u * component)) & 3u)) & 1u) != 0) mask = static_cast(mask | (0xfu << (4u * component))); + } + return mask; +} + +ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { + ShaderRecompiler::TargetMerge merge; + for (const auto& color : state.colors) { + if (color.partialWriteMask == 0) continue; + Require(merge.mask == 0 && color.exportIndex < 8u && color.samples == 1u, "a draw merges into one single-sample packed color target at most"); + merge.target = static_cast(color.exportIndex); + merge.mask = color.partialWriteMask; + merge.samples = static_cast(color.samples); + merge.descriptor = EncodeTextureResource(SurfaceForTarget(color)); + } + return merge; +} + bool PartialSampleMask(const State& state) { const auto full = (1u << state.samples) - 1u; return std::ranges::any_of(state.quadSampleMask, [&](std::uint16_t mask) { return (mask & full) != full; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 79c1f4a4ef..b40bbce37e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8803,9 +8803,82 @@ void unorm4_4_4_4TargetTests() { Require(state.color.format == VK_FORMAT_R16_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0x1bu && state.color.packing == ColorExportPacking::Unorm4_4_4_4, "a 4_4_4_4 unorm target did not decode to an R16_UINT attachment with reversed exports"); const auto packings = AgcDriver::Graphics::ExportPackings(state); Require(packings[0] == ColorExportPacking::Unorm4_4_4_4 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 4_4_4_4 unorm packing"); + Require(state.color.partialWriteMask == 0u && state.blends[0].colorWriteMask == 0xfu && AgcDriver::Graphics::ExportTargetMerge(state).mask == 0u, "a full write of a 4_4_4_4 unorm target merged into the target"); auto changed = queue; - changed.context[0x8e] = 7; - expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a 4_4_4_4 unorm color target"); + for (const auto& [targetMask, wordMask] : {std::pair{1u, 0xf000u}, std::pair{7u, 0xfff0u}, std::pair{8u, 0x000fu}, std::pair{0xau, 0x0f0fu}}) { + changed.context[0x8e] = targetMask; + const auto partial = AgcDriver::Graphics::DecodeState(changed); + Require(partial.color.partialWriteMask == wordMask && partial.colors.front().partialWriteMask == wordMask && partial.blends[0].colorWriteMask == 0u && partial.blend.colorWriteMask == 0u, "CB_TARGET_MASK " + std::to_string(targetMask) + " of a 4_4_4_4 unorm target did not merge its nibbles behind a closed write mask"); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); + const auto expected = AgcDriver::Graphics::SurfaceForTarget(partial.color); + const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); + Require(merge.target == 0u && merge.mask == wordMask && merge.samples == 1u && surface.baseAddress == expected.baseAddress && surface.pipeBankXor == expected.pipeBankXor && surface.width == expected.width && surface.height == expected.height && surface.format == expected.format && surface.tileMode == expected.tileMode && surface.dimension == AgcDriver::Graphics::TextureDimension::k2D && surface.mipCount == 1u && surface.samples == 1u, "the merge descriptor of CB_TARGET_MASK " + std::to_string(targetMask) + " does not decode to the target's surface"); + } + { + changed.context[0x8e] = 1; + const auto partial = AgcDriver::Graphics::DecodeState(changed); + constexpr std::array mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u}; + auto merging = packedExportRequest(changed, partial, mergeCode); + merging.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(partial); + const std::array capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat}; + merging.target.supportedCapabilities = capabilities; + merging.target.registrationWordLimit = 1u << 20u; + const ShaderRecompiler::RequestSerializer mergeSerializer; + Require(mergeSerializer.Deserialize(mergeSerializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "the 4_4_4_4 target merge was lost in serialization"); + auto unmerged = merging; + unmerged.context.pixel->targetMerge = {}; + auto moved = merging; + moved.context.pixel->targetMerge.descriptor[0] += 0x100u; + std::vector mergedKey, unmergedKey, movedKey; + ShaderRecompiler::RecompileCacheKey::BuildInterface(merging, mergedKey); + ShaderRecompiler::RecompileCacheKey::BuildInterface(unmerged, unmergedKey); + ShaderRecompiler::RecompileCacheKey::BuildInterface(moved, movedKey); + Require(mergedKey != unmergedKey && mergedKey == movedKey, "the static shader key does not separate merging from plain writes, or depends on the merged target's address"); + for (const bool registration : {false, true}) { + merging.target.registrationModules = registration; + const auto merged = ShaderRecompiler::Recompile(merging); + const auto words = merged.spirv.Copy(); + std::size_t reads = 0, writes = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + reads += static_cast(words[at] & 0xffffu) == spv::OpImageRead ? 1u : 0u; + writes += static_cast(words[at] & 0xffffu) == spv::OpImageWrite ? 1u : 0u; + } + const auto what = std::string(registration ? "a registration" : "a specialized") + " 4_4_4_4 merge module"; + Require(merged.registrationModule == registration && reads == 1u && writes == 1u && interlockShape(words) == "capability extension ordered begin end " && declaresEarlyFragmentTests(merging), what + " did not read and write the target once inside a pixel interlock after early tests: " + interlockShape(words)); + const auto bound = std::ranges::any_of(merged.bindings, [&](const ShaderRecompiler::DescriptorBinding& binding) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages || binding.kind != ShaderRecompiler::DescriptorKind::StorageImage) return false; + for (std::size_t offset = 0; offset + 8u <= binding.guestDescriptor.size(); offset += 8u) { + if (std::equal(merging.context.pixel->targetMerge.descriptor.begin(), merging.context.pixel->targetMerge.descriptor.end(), binding.guestDescriptor.begin() + static_cast(offset))) return true; + } + return false; + }); + Require(bound, what + " does not bind the merged target as a storage image"); + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &merged, 0}}}; + Require(AgcDriver::Graphics::ValidateShaders(shaders, partial, subgroup, false) == std::set{0u}, what + " lost its export to attachment 0"); + } + auto lacking = merging; + const std::array noInterlock{spv::CapabilityShader, spv::CapabilityStorageImageReadWithoutFormat}; + lacking.target.supportedCapabilities = noInterlock; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(lacking)); }, "fragmentShaderPixelInterlock"); + auto unformatted = merging; + const std::array noFormatlessReads{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT}; + unformatted.target.supportedCapabilities = noFormatlessReads; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(unformatted)); }, "storage image reads without a format"); + auto killing = merging; + killing.context.pixel->pixelKillEnable = true; + killing.context.pixel->earlyZ = false; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(killing)); }, "kills pixels under late tests"); + auto exporting = merging; + exporting.context.pixel->depthExportEnable = true; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(exporting)); }, "exports depth"); + } + changed = queue; + changed.context[0x8e] = 0xau; + changed.context[0x31d] = (changed.context.at(0x31d) & ~0x3f000u) | (1u << 12u) | (1u << 15u); + expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a multisampled 4_4_4_4 unorm color target"); for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { changed = queue; changed.context[0x31c] = (changed.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 09ada8fddd..bce4131fdc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4746,7 +4746,7 @@ void verifyPixelRequestSerialization() { value.posZ, value.posW, value.frontFace, value.frontFaceAllBits, value.ancillary, value.sampleShading, value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable, value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport, value.orderedPixelShader, - value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage, value.reversedBlend); + value.targetOutputMode, value.targetExportMapping, value.alphaToCoverage, value.stencilRefExportEnable, value.rasterPrimitive, value.layerExported, value.ditheredAlphaToCoverage, value.reversedBlend, value.targetMerge); }; const std::array code{0xbf810000u}; const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; @@ -4794,6 +4794,7 @@ void verifyPixelRequestSerialization() { pixel.stencilRefExportEnable = true; pixel.ditheredAlphaToCoverage = true; pixel.reversedBlend = 0x570001u; + pixel.targetMerge = {1u, 0x0ff0u, 1u, {0x100u, 0x2u, 0x3u, 0x4u, 0x5u, 0x6u, 0x7u, 0x8u}}; const std::array, 3> mappings{{ {0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau}, {0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u}, @@ -4826,6 +4827,14 @@ void verifyPixelRequestSerialization() { withoutReversedBlend.context.pixel->reversedBlend = 0u; RecompileCacheKey::Build(withoutReversedBlend, replayKey); require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(withoutReversedBlend), "the reversed-order blend code changed the static shader identity"); + auto movedMerge = request; + movedMerge.context.pixel->targetMerge.descriptor[0] = 0x200u; + RecompileCacheKey::Build(movedMerge, replayKey); + require(key == replayKey, "the merged target's descriptor changed the static shader identity"); + auto withoutMerge = request; + withoutMerge.context.pixel->targetMerge = {}; + RecompileCacheKey::Build(withoutMerge, replayKey); + require(key != replayKey, "a color target merge did not change the static shader identity"); auto withoutCoverage = request; withoutCoverage.context.pixel->alphaToCoverage = false; RecompileCacheKey::Build(withoutCoverage, replayKey); @@ -4848,7 +4857,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQScAAAA=", "new requests did not use serialization version 39"); + require(requestPrefix(encoded, 8u) == "NVNQQSgAAAA=", "new requests did not use serialization version 40"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -4966,7 +4975,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSgAAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSkAAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5104,8 +5113,23 @@ void verifyVertexInfoVersion19() { while (allBitsOffset < plainPixel.size() && plainPixel[allBitsOffset] == allBitsBytes[allBitsOffset]) ++allBitsOffset; require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks"); - constexpr std::size_t sampleMaskBytes = 16u; constexpr std::size_t iterationBytes = 35u; + constexpr std::size_t mergeBytes = 38u; + const auto mergeOffset = static_cast(blendOffset + 4u + iterationBytes); + auto merging = pixel; + merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}}; + const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); + require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 38 bytes the version 40 pixel info adds after the pixel iteration fields"); + require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); + const auto withoutMerge = [&](std::vector bytes) { + bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast(mergeBytes)); + return bytes; + }; + auto version39 = withoutMerge(plainPixel); + version39[4] = 39u; + const auto replay39 = serializer.Deserialize(encodeBase64(version39)); + require(replay39.request.context.pixel.has_value() && replay39.request.context.pixel->targetMerge == TargetMerge{} && serializer.Serialize(replay39.request) == encodedPixel, "a version 39 request read a color target merge or misread the fields after it"); + constexpr std::size_t sampleMaskBytes = 16u; auto maskedPixel = pixel; maskedPixel.context.pixel->quadPixelMask = 0x2u; const auto maskedBytes = decodeBase64(serializer.Serialize(maskedPixel)); @@ -5116,7 +5140,7 @@ void verifyVertexInfoVersion19() { sampledPixel.context.pixel->quadSampleMask = {0x1u, 0x2u, 0x4u, 0x8u}; const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel)); require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); - auto version30 = sampledBytes; + auto version30 = withoutMerge(sampledBytes); version30.pop_back(); version30.pop_back(); version30.pop_back(); @@ -5126,7 +5150,7 @@ void verifyVertexInfoVersion19() { require(replay30.request.context.pixel.has_value() && !replay30.request.context.pixel->iteratesSamples && replay30.request.context.pixel->positionLocation == PixelPositionLocation::Center && replay30.request.context.pixel->sampleLocations == std::array{} && decodeBase64(serializer.Serialize(replay30.request)) == sampledBytes, "a version 30 request read pixel iteration, a position location or sample locations, or misread the fields after its reversed-order blend code"); auto spanned = sampledPixel; spanned.target.spannedWaves = true; - auto version32 = decodeBase64(serializer.Serialize(spanned)); + auto version32 = withoutMerge(decodeBase64(serializer.Serialize(spanned))); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); @@ -5134,12 +5158,12 @@ void verifyVertexInfoVersion19() { version32[4] = 32u; const auto replay32 = serializer.Deserialize(encodeBase64(version32)); require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag"); - auto version29 = sampledBytes; + auto version29 = withoutMerge(sampledBytes); version29.pop_back(); version29.pop_back(); version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes)); - auto version36 = sampledBytes; + auto version36 = withoutMerge(sampledBytes); version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u)); version36[4] = 36u; const auto replay36 = serializer.Deserialize(encodeBase64(version36)); @@ -5147,7 +5171,7 @@ void verifyVertexInfoVersion19() { version29[4] = 29u; const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); - auto version28 = sampledBytes; + auto version28 = withoutMerge(sampledBytes); version28.pop_back(); version28.pop_back(); version28.pop_back(); @@ -5155,7 +5179,7 @@ void verifyVertexInfoVersion19() { version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); - auto version27 = maskedBytes; + auto version27 = withoutMerge(maskedBytes); version27.pop_back(); version27.pop_back(); version27.pop_back(); @@ -5163,7 +5187,7 @@ void verifyVertexInfoVersion19() { version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); - auto legacyPixel = plainPixel; + auto legacyPixel = withoutMerge(plainPixel); legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp new file mode 100644 index 0000000000..0a3426612b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -0,0 +1,324 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 64> Programs{}; +std::size_t programCount = 0; + +std::uint32_t Unorm(float value) { + if (!(value > 0.0f)) return 0; + if (value >= 1.0f) return 15; + return static_cast(std::nearbyint(static_cast(value) * 15.0)); +} + +std::uint32_t Packed(std::array rgba) { + return Unorm(rgba[3]) | (Unorm(rgba[2]) << 4u) | (Unorm(rgba[1]) << 8u) | (Unorm(rgba[0]) << 12u); +} + +std::uint32_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); +} + +std::span Program(const std::array& vgprs, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = vgprs[vgpr]; + } + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span Float32Program(std::array rgba) { + return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false); +} + +std::span Float16Program(std::array rgba) { + return Program({Half(rgba[0]) | (Half(rgba[1]) << 16u), Half(rgba[2]) | (Half(rgba[3]) << 16u), 0, 0}, true); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm4_4_4_4Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.tileMode == ColorTileMode::RenderTarget && color.bytes == block.bytes, "the tiled 4_4_4_4 unorm target did not decode as a packed R16_UINT target"); + return color; +} + +std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + const bool partial = exportedMask != 0xfu; + color.partialWriteMask = partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = partial ? 0u : VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); + Require((fragment.context.pixel->targetMerge.mask != 0u) == partial && fragment.context.pixel->targetMerge.mask == color.partialWriteMask, "the draw's target merge disagrees with its write mask"); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const bool registered = pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId; + Require(registered != partial, partial ? "the merging draw reused the registered pixel shader" : "the full write did not use the registered pixel shader"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + return color.partialWriteMask; +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool inside = x < ScissorWidth; + const std::uint32_t expected = inside ? (before[index] & ~wordMask) | (word & wordMask) : before[index]; + if (stored[index] == expected) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, inside ? "inside" : "outside", stored[index], expected, before[index]); + throw std::runtime_error(what + detail); + } + } +} + +std::vector Fill(Block& block, std::uint32_t seed) { + std::vector words(block.bytes / 2u); + for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast(((index + seed) * 0x9e3779b9u) >> 11u); + std::memcpy(block.data, words.data(), block.bytes); + return words; +} + +std::string Describe(std::uint32_t mask, std::array rgba) { + char text[160]; + std::snprintf(text, sizeof(text), "CB_TARGET_MASK 0x%x write of (%.9g, %.9g, %.9g, %.9g)", mask, rgba[0], rgba[1], rgba[2], rgba[3]); + return text; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT)) { + std::puts("skipped, the device lacks fragmentShaderPixelInterlock"); + return VulkanTestSkipped; + } + const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 2); + Block block(layout.Bytes()); + const auto color = DecodeTarget(block); + Require(AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 1u) == 0xf000u && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 8u) == 0x000fu && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 5u) == 0xf0f0u, "the export components did not map to the guest nibbles (X in 15..12, W in 3..0)"); + struct Case { + std::uint32_t mask; + std::array rgba; + }; + const std::array cases{{ + {0x1u, {1.0f, 0.0f, 0.0f, 0.0f}}, + {0x1u, {7.0f / 15.0f, 1.0f, 1.0f, 1.0f}}, + {0x5u, {0.2f, 0.4f, 0.6f, 0.8f}}, + {0x2u, {0.0f, 2.0f / 15.0f, 1.0f, 1.0f}}, + {0x8u, {1.0f, 1.0f, 1.0f, 11.0f / 15.0f}}, + {0x6u, {0.0f, 0.0f, 0.0f, 0.0f}}, + {0xeu, {0.0f, 1.0f, 0.5f, 0.25f}}, + {0xfu, {0.1f, 0.7f, 0.9f, 0.3f}}, + {0x1u, {0.0f, 0.0f, 0.0f, 0.0f}}, + }}; + auto before = Fill(block, 0); + for (const auto& [mask, rgba] : cases) { + const auto wordMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba)); + ExpectSurface(*device, block, layout, before, Packed(rgba), mask == 0xfu ? 0xffffu : wordMask, "a 32_ABGR " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + before = ReadBack(*device, block); + } + before = Fill(block, 77); + const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; + const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves)); + ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws merged into a %ux%u tiled target, the pixels outside the scissor kept\n", cases.size() + 1u, Width, Height); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 19711b8ddc..34f15975af 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -217,6 +217,9 @@ private: append(key, value.positionLocation); append(key, value.sampleLocations); append(key, value.targetOutputMode); + append(key, value.targetMerge.target); + append(key, value.targetMerge.mask); + append(key, value.targetMerge.samples); } static void append(std::vector& key, const ShaderVertexResourceDestination& value) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 34d40e5e35..73fd3d1ce0 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -355,6 +355,10 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU8(static_cast(info.positionLocation)); for (const std::uint8_t location : info.sampleLocations) writer.WriteU8(location); writer.WriteBool(info.stencilOperationValueExport); + writer.WriteU8(info.targetMerge.target); + writer.WriteU32(info.targetMerge.mask); + writer.WriteU8(info.targetMerge.samples); + for (const std::uint32_t word : info.targetMerge.descriptor) writer.WriteU32(word); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -423,6 +427,14 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { for (std::uint8_t& location : info.sampleLocations) location = reader.ReadU8(); } if (version >= 37u) info.stencilOperationValueExport = reader.ReadBool(); + if (version >= 40u) { + info.targetMerge.target = reader.ReadU8(); + const auto mask = reader.ReadU32(); + if (mask > 0xffffu) throw std::runtime_error("invalid recompile request target merge mask"); + info.targetMerge.mask = static_cast(mask); + info.targetMerge.samples = reader.ReadU8(); + for (std::uint32_t& word : info.targetMerge.descriptor) word = reader.ReadU32(); + } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -827,7 +839,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(39u); + writer.WriteU32(40u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -866,7 +878,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 39u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 40u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 2ff9d9a15a..f4c2a4e3a3 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -104,6 +104,7 @@ struct ImageResource { std::uint32_t emulatedCompare = 0; std::uint32_t table = NoTable; TableOperation tableOperation = TableOperation::Supported; + bool targetMerge = false; bool operator==(const ImageResource& other) const = default; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index f2a9ff9323..99f08344b9 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -195,6 +195,9 @@ struct ShaderPixelInputInfo { bool psIteratesSamples = false; PixelPositionLocation psPositionLocation = PixelPositionLocation::Center; std::array sampleLocations{}; + std::uint8_t mergeTarget = 0; + std::uint16_t mergeMask = 0; + std::uint8_t mergeSamples = 1; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3a9a072113..a3837600de 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1831,6 +1831,7 @@ std::shared_ptr ResourceMaterializer::ResolveImageTab } std::vector ResourceMaterializer::RuntimeImageModes(const ImageResource& image) { + if (image.targetMerge) return {image}; const bool table = image.table != NoTable; if (table && image.tableOperation == TableOperation::Unsupported) return {}; std::vector modes; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index d94eaff86a..b56aea041c 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -1,4 +1,5 @@ #include "Optimization/ShaderInfoCollector.hpp" +#include "Optimization/ResourceMaterializer.hpp" #include #include #include @@ -519,6 +520,24 @@ std::uint16_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage s return static_cast(use); } +void AddTargetMerge(const ShaderPixelInputInfo& pixel, ShaderInfo& info) { + if (info.runtimeImageModes.size() != info.images.size() || std::ranges::any_of(info.images, [](const ImageResource& image) { return image.targetMerge; })) { + return Fail("color target merge image cannot join the prepared images"); + } + ImageResource image; + image.source = std::numeric_limits::max(); + image.resourceClass = ImageResourceClass::Storage; + image.numericClass = IrTextureNumericClass::Uint; + image.dimension = RdnaImageDimension::Dim2D; + image.read = true; + image.written = true; + image.fmaskCompatible = false; + image.targetMerge = true; + info.images.push_back(image); + info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); + AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); +} + void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { for (auto& sampler : info.samplers) { sampler.uses = 0; @@ -588,6 +607,7 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo } CollectBuiltinInputs(program, next); CollectOutputs(program, inputInfo, next); + if (program.Resources().stage == IrShaderStage::Pixel && inputInfo.pixel->mergeMask != 0u) AddTargetMerge(*inputInfo.pixel, next); CollectSamplerUses(program, next); program.Info() = std::move(next); program.Metadata().shaderInfoComplete = true; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 3731da950d..d9dd6c8da8 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -959,7 +959,24 @@ std::shared_ptr specializeModule(const CompiledShaderAr return entry->module; } -RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults = false) { +const ResourceSnapshot& mergeTargetImage(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& snapshot, bool registrationDefaults, ResourceSnapshot& merged) { + const auto& images = variant.info.info.images; + const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); + if (image == images.end() || registrationDefaults) return snapshot; + if (!request.context.pixel || request.context.pixel->targetMerge.mask == 0u) throw std::runtime_error("ShaderRecompiler: a color target merge variant was materialized without the merged target"); + if (image + 1 != images.end() || snapshot.images.size() + 1u != images.size()) throw std::runtime_error("ShaderRecompiler: the captured images do not precede the color target merge image"); + merged = snapshot; + DescriptorValue descriptor; + std::ranges::copy(request.context.pixel->targetMerge.descriptor, descriptor.dwords.begin()); + descriptor.dwordCount = 8u; + descriptor.availableDwordMask = 0xffu; + merged.images.push_back(descriptor); + return merged; +} + +RecompileResult materializeResult(const CompiledVariant& variant, const RecompileRequest& request, const ResourceSnapshot& captured, bool registrationDefaults = false) { + ResourceSnapshot merged; + const auto& snapshot = mergeTargetImage(variant, request, captured, registrationDefaults, merged); RecompileResult result; const auto& artifact = variant.artifact; static_cast(result) = { @@ -1371,6 +1388,11 @@ std::uint64_t snapshotHash(const RecompileRequest& request, const ResourceSnapsh mix(request.context.pixel->reversedBlend); mix(static_cast(request.context.pixel->rasterPrimitive)); mix(request.context.pixel->layerExported); + const auto& merge = request.context.pixel->targetMerge; + mix(merge.target); + mix(merge.mask); + mix(merge.samples); + for (const auto word : merge.descriptor) mix(word); } if (request.context.vertex) { const auto& vertex = *request.context.vertex; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index b167b6c7f1..9db85f8311 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -151,6 +151,15 @@ inline constexpr std::uint32_t One = 3u; } +struct TargetMerge { + std::uint8_t target = 0; + std::uint16_t mask = 0; + std::uint8_t samples = 1; + std::array descriptor{}; + + bool operator==(const TargetMerge&) const = default; +}; + struct ShaderPixelStageInfo { std::uint32_t interpolatorCount; std::array interpolatorSettings; @@ -191,6 +200,7 @@ struct ShaderPixelStageInfo { bool iteratesSamples = false; PixelPositionLocation positionLocation = PixelPositionLocation::Center; std::array sampleLocations{}; + TargetMerge targetMerge{}; }; struct ShaderVertexBufferResource { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index b5bd3c4436..dbc6a40010 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -140,7 +140,7 @@ constexpr std::size_t fieldCount() { static_assert(fieldCount() == 23, "ShaderInfo changed: update the info encoder"); static_assert(fieldCount() == 12, "BufferResource changed: update the info encoder"); -static_assert(fieldCount() == 40, "ImageResource changed: update the info encoder"); +static_assert(fieldCount() == 41, "ImageResource changed: update the info encoder"); static_assert(fieldCount() == 8, "SamplerResource changed: update the info encoder"); static_assert(fieldCount() == 3, "SampledResourcePair changed: update the info encoder"); static_assert(fieldCount() == 5, "StageInput changed: update the info encoder"); @@ -751,6 +751,7 @@ void encodeShaderInfo(Writer& writer, const ShaderInfo& info) { out.Value(image.emulatedCompare); out.Value(image.table); out.Value(image.tableOperation); + out.Value(image.targetMerge); }); writer.Value(!info.runtimeImageModes.empty()); writer.List(info.samplers, [](Writer& out, const SamplerResource& sampler) { @@ -855,6 +856,7 @@ void decodeShaderInfo(Reader& reader, ShaderInfo& info) { in.Value(image.emulatedCompare); in.Value(image.table); in.Value(image.tableOperation); + in.Value(image.targetMerge); }); bool preparedImageModes = false; reader.Value(preparedImageModes); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 5543565717..5d721bb300 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 41; +inline constexpr std::uint32_t FormatVersion = 42; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 96951cf815..c210de9ddd 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -70,6 +70,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); bool TablePointSampled(const ImageResource& image); bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); std::uint32_t ExecutionModelForStage(IrShaderStage stage); std::uint32_t ConstantI32(SpirvEmitterState& state, std::int32_t value); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index b38633af2e..766da39c29 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -21,6 +21,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); bool TablePointSampled(const ImageResource& image); bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension); [[noreturn]] void ExitDescriptorBindingFailure(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource, const char* reason); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index 946baa6068..c9a3717039 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -233,6 +233,22 @@ std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint3 return DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "storage image descriptor array was not emitted"); } +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord) { + const auto& image = state.program.Info().images.at(resource); + if (image.resourceClass != ImageResourceClass::Storage || image.table != NoTable || image.mipMode == ImageMipMode::DynamicStorage || image.atomic) { + FailEmit("storage image read requested for an image that is not one plain storage descriptor"); + } + if (std::ranges::find(state.supportedCapabilities, spv::CapabilityStorageImageReadWithoutFormat) == state.supportedCapabilities.end()) FailEmit("storage image reads without a format are unavailable on the target device"); + state.module.EmitCapability(spv::CapabilityStorageImageReadWithoutFormat); + const auto imageType = ImageType(state, image); + const auto descriptor = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, imageType, descriptor, StorageImageDescriptorPointer(state, resource)); + const auto texel = state.module.AllocateId(); + const auto texelType = ImageVectorType(state, image.numericClass, 4u); + state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord); + return texel; +} + void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample) { const auto& image = state.runtimeImage != nullptr ? *state.runtimeImage : state.program.Info().images.at(resource); if (image.resourceClass != ImageResourceClass::Storage) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 4c05ffe094..1246e7cbe6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -150,7 +150,7 @@ namespace ShaderRecompiler } bool OrderedPixelShader(const SpirvEmitterState& state) { - return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psOrderedPixelShader; + return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && (state.inputInfo.pixel->psOrderedPixelShader || state.inputInfo.pixel->mergeMask != 0u); } const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9bbf4fb15b..697a656367 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -383,12 +383,16 @@ void DefineModule(SpirvEmitterState& state) { state.module.EmitExtension("SPV_EXT_shader_stencil_export"); state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } - if (pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) { + const bool merge = pixel.mergeMask != 0u; + if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ))) { + throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); + } + if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } - if (pixel.psOrderedPixelShader) { + if (pixel.psOrderedPixelShader || merge) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityFragmentShaderPixelInterlockEXT)) == state.supportedCapabilities.end()) { - throw std::runtime_error("a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks"); + throw std::runtime_error(merge ? "a partial write of a packed color target needs the fragmentShaderPixelInterlock feature, which the device lacks" : "a primitive-ordered pixel shader needs the fragmentShaderPixelInterlock feature, which the device lacks"); } state.module.EmitCapability(spv::CapabilityFragmentShaderPixelInterlockEXT); state.module.EmitExtension("SPV_EXT_fragment_shader_interlock"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 49cbac0bcf..f657f4122a 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -414,6 +414,28 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target return result; } +void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) { + const auto& pixel = PixelInfo(state); + const auto& images = state.program.Info().images; + const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); + if (image == images.end()) FailEmit("the color target merge image was not collected"); + if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u) FailEmit("the color target merge inputs were not declared"); + const auto resource = static_cast(image - images.begin()); + const auto position = [&](std::uint32_t component) { return Unary(state, spv::OpConvertFToU, TypeU32(state), Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component))); }; + const auto x = position(0u); + const auto y = position(1u); + const auto coord = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2u), coord, x, y); + const auto stored = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord), 0u); + const auto word = PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color); + const auto kept = EmitBinaryU32(state, spv::OpBitwiseAnd, stored, ConstantU32(state, ~static_cast(pixel.mergeMask))); + const auto merged = EmitOrU32(state, kept, EmitBinaryU32(state, spv::OpBitwiseAnd, word, ConstantU32(state, pixel.mergeMask))); + const auto texel = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), texel, merged, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)); + EmitStorageImageWrite(state, resource, 0u, coord, texel, 0u); +} + bool AlphaToCoverage(const SpirvEmitterState& state) { return state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->psAlphaToCoverage; } @@ -1847,6 +1869,10 @@ void EmitSetAttribute(SpirvValueEmitContext& ctx, const IrValue& inst) { value = mapped; if (exp.index == 0u && !uintOutput && AlphaToCoverage(state)) StoreAlphaCoverage(state, value); const auto mode = MrtOutputMode(state, exp); + if (PixelInfo(state).mergeMask != 0u && exp.index == PixelInfo(state).mergeTarget) { + if (uintOutput || mode == 5u || mode == 6u) FailEmit("a partial write of a packed color target cannot take an integer or 16-bit normalized export"); + MergeColorTarget(state, value); + } if (!uintOutput && mode != 5u && mode != 6u) value = SelectExportPacking(state, exp.index, value); } if (exp.kind == ExportTargetKind::Position) { diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 6c894d3e8c..98b1fb91ee 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -155,6 +155,10 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psIteratesSamples = pixel.iteratesSamples; pixelStorage.psPositionLocation = pixel.positionLocation; pixelStorage.sampleLocations = pixel.sampleLocations; + if (pixel.targetMerge.mask != 0u && (pixel.targetMerge.target >= 8u || pixel.targetMerge.samples != 1u)) throw std::runtime_error("ShaderInputInfoBuilder: invalid color target merge"); + pixelStorage.mergeTarget = pixel.targetMerge.target; + pixelStorage.mergeMask = pixel.targetMerge.mask; + pixelStorage.mergeSamples = pixel.targetMerge.samples; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 138602235a..b3482d7888 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -54,7 +54,8 @@ void InsertMeshBarycentricFlag(std::vector& bytes) { void InsertPixelDefaultsAfterFrontFace(std::vector& bytes) { bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u}); - bytes.insert(bytes.begin() + 282u, 35u, 0u); + bytes.insert(bytes.begin() + 282u, 73u, 0u); + bytes[322] = 1u; } std::vector DecodeBase64(std::string_view text) { @@ -214,7 +215,7 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 39u; + upgraded[4] = 40u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); @@ -254,7 +255,7 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 39u; + layered[4] = 40u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); @@ -280,7 +281,7 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 39u; + bytes[4] = 40u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); @@ -410,7 +411,8 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38); auto upgraded = main38; - upgraded[4] = 39u; + if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 355u); + upgraded[4] = 40u; if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); upgraded.insert(upgraded.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); @@ -429,7 +431,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 39u; + main25[4] = 40u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); @@ -451,9 +453,9 @@ int main() { const auto replay = serializer.Deserialize(encoded); Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 39u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 40u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto future = bytes; - future[4] = 40u; + future[4] = 41u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -498,7 +500,7 @@ int main() { auto version35 = DecodeBase64(serializer.Serialize(request)); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); Require(stencilOperationValue < version35.end() && *stencilOperationValue == 0u); - version35.erase(stencilOperationValue); + version35.erase(stencilOperationValue, stencilOperationValue + 1 + 38); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); From b8a21d469c7fbfe137f7c51d159e659e23400b05 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:12:37 +0200 Subject: [PATCH 043/318] test(agc): draw merged 4_4_4_4 writes back to back and through the precheck Merged and full writes into one target are now also drawn without a wait between them and checked once at the end, so the render pass that follows a merging draw has to end on the storage write to its attachment. The unit test also passes the partial target masks through DrawRejection. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 1 + .../execution/PackedUnorm4444PartialWrite.cpp | 45 ++++++++++++++++--- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index b40bbce37e..59d70fd4a6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8807,6 +8807,7 @@ void unorm4_4_4_4TargetTests() { auto changed = queue; for (const auto& [targetMask, wordMask] : {std::pair{1u, 0xf000u}, std::pair{7u, 0xfff0u}, std::pair{8u, 0x000fu}, std::pair{0xau, 0x0f0fu}}) { changed.context[0x8e] = targetMask; + Require(AgcDriver::Graphics::DrawRejection(changed, false).empty(), "the draw precheck refused CB_TARGET_MASK " + std::to_string(targetMask) + " of a 4_4_4_4 unorm target"); const auto partial = AgcDriver::Graphics::DecodeState(changed); Require(partial.color.partialWriteMask == wordMask && partial.colors.front().partialWriteMask == wordMask && partial.blends[0].colorWriteMask == 0u && partial.blend.colorWriteMask == 0u, "CB_TARGET_MASK " + std::to_string(targetMask) + " of a 4_4_4_4 unorm target did not merge its nibbles behind a closed write mask"); const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 0a3426612b..73696717be 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -270,6 +270,36 @@ std::vector Fill(Block& block, std::uint32_t seed) { return words; } +struct Case { + std::uint32_t mask; + std::array rgba; +}; + +void BackToBack(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, std::span draws, std::uint32_t seed, const std::string& what) { + const auto before = Fill(block, seed); + auto expected = before; + for (const auto& [mask, rgba] : draws) { + const auto drawn = Draw(device, color, mask, Float32Export, Float32Program(rgba)); + const auto wordMask = mask == 0xfu ? 0xffffu : drawn; + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < ScissorWidth; ++x) { + auto& word = expected[layout.Offset(x, y) / 2u]; + word = static_cast((word & ~wordMask) | (Packed(rgba) & wordMask)); + } + } + } + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + if (stored[index] == expected[index]) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, stored[index], expected[index], before[index]); + throw std::runtime_error(what + detail); + } + } +} + std::string Describe(std::uint32_t mask, std::array rgba) { char text[160]; std::snprintf(text, sizeof(text), "CB_TARGET_MASK 0x%x write of (%.9g, %.9g, %.9g, %.9g)", mask, rgba[0], rgba[1], rgba[2], rgba[3]); @@ -290,10 +320,6 @@ int main() { Block block(layout.Bytes()); const auto color = DecodeTarget(block); Require(AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 1u) == 0xf000u && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 8u) == 0x000fu && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 5u) == 0xf0f0u, "the export components did not map to the guest nibbles (X in 15..12, W in 3..0)"); - struct Case { - std::uint32_t mask; - std::array rgba; - }; const std::array cases{{ {0x1u, {1.0f, 0.0f, 0.0f, 0.0f}}, {0x1u, {7.0f / 15.0f, 1.0f, 1.0f, 1.0f}}, @@ -311,11 +337,20 @@ int main() { ExpectSurface(*device, block, layout, before, Packed(rgba), mask == 0xfu ? 0xffffu : wordMask, "a 32_ABGR " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); before = ReadBack(*device, block); } + const std::array b{2.0f / 15.0f, 1.0f, 1.0f, 1.0f}; + const std::array c{0.6f, 0.2f, 0.8f, 0.4f}; + const std::array d{1.0f, 0.0f, 9.0f / 15.0f, 0.0f}; + const std::array mergeThenFull{{{0x1u, b}, {0xfu, c}}}; + const std::array fullThenMerge{{{0xfu, c}, {0x1u, b}}}; + const std::array mergeThenMerge{{{0x1u, b}, {0x4u, d}, {0x8u, c}}}; + BackToBack(*device, block, layout, color, mergeThenFull, 11, "a merged X write followed at once by a full write"); + BackToBack(*device, block, layout, color, fullThenMerge, 12, "a full write followed at once by a merged X write"); + BackToBack(*device, block, layout, color, mergeThenMerge, 13, "merged X, Z and W writes drawn back to back"); before = Fill(block, 77); const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves)); ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); - std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws merged into a %ux%u tiled target, the pixels outside the scissor kept\n", cases.size() + 1u, Width, Height); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws and 3 back-to-back sequences merged into a %ux%u tiled target, the pixels outside the scissor kept\n", cases.size() + 1u, Width, Height); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From eecf6fcff4e36f3ab87348ec574da3b95a68cc77 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:22:37 +0200 Subject: [PATCH 044/318] test(agc): render a 4_4_4_4 UNORM color target and read back the stored words Draws 32_ABGR and FP16_ABGR exports into a linear 4_4_4_4 target and checks every pixel against the guest layout (A in bits 3..0, B in 7..4, G in 11..8, R in 15..12), including NaN, negative, infinite, denormal and rounded values, and the pixels outside the scissor. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../execution/PackedUnorm4444ColorTarget.cpp | 330 ++++++++++++++++++ 2 files changed, 332 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444ColorTarget.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 66a438c1fb..79e28aa9b6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1954,6 +1954,8 @@ target_link_libraries(agc_driver_job_manager_compute_tests PRIVATE libSceJobMana target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $) +target_include_directories(agc_driver_packed_unorm4444_color_target_tests PRIVATE $) + target_include_directories(agc_driver_color1555_target_tests PRIVATE $) target_include_directories(agc_driver_color565_target_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444ColorTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444ColorTarget.cpp new file mode 100644 index 0000000000..85326136cf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444ColorTarget.cpp @@ -0,0 +1,330 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::ColorTargetLayout; +using AgcDriver::Graphics::ColorTileMode; +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 256; +constexpr std::uint32_t Height = 128; +constexpr std::uint32_t ScissorWidth = 128; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); +constexpr std::uint32_t LinearAttrib3 = 0x09000000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Float16Export = 4; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +constexpr std::array, 3> Triangle{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 32> Programs{}; +std::size_t programCount = 0; + +struct Golden { + std::array rgba; + std::uint16_t word; +}; + +std::uint32_t Half(float value) { + Require(value == 0.0f || (value >= 0.0625f && value <= 1.0f), "the test's half values are normal and at most 1"); + if (value == 0.0f) return 0; + const auto bits = std::bit_cast(value); + Require((bits & 0x1fffu) == 0, "the test's half values are exact"); + return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); +} + +std::span Program(const std::array& vgprs, bool compressed) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = vgprs[vgpr]; + } + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +std::span Float32Program(std::array rgba) { + return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false); +} + +std::span Float16Program(std::array rgba) { + return Program({Half(rgba[0]) | (Half(rgba[1]) << 16u), Half(rgba[2]) | (Half(rgba[3]) << 16u), 0, 0}, true); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { +#ifdef _WIN32 + if (watched) { + data = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes)); + GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes); + } else { + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + } +#else + if (watched) { + void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Require(raw != MAP_FAILED, "cannot map the color target"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + BlockBytes - 1) & ~(static_cast(BlockBytes) - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast(aligned + bytes), begin + BlockBytes - aligned); + data = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes); + } else { + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); + } +#endif + Require(data != nullptr, "cannot allocate the color target"); + Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched"); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + if (watched) { + GuestArena::GuestArenaReset_nid_postfix(data, bytes); + GuestArena::GuestArenaRelease_nid_postfix(data, bytes); + } else { + VirtualFree(data, 0, MEM_RELEASE); + } +#else + if (watched) { + munmap(data, bytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes); + } else { + std::free(data); + } +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + bool watched; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm4_4_4_4Info; + cx[0x31d] = 0; + cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u); + cx[0x3b8] = attrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16_UINT && color.elementBytes == 2 && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.bytes == block.bytes, "the 4_4_4_4 unorm target did not decode as a packed R16_UINT target"); + return color; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t exportFormat, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(Triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {ScissorWidth, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) { + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request); + return invocation.Materialize(*invocation.Capture(runtime)); + }; + const auto pixelResult = materialize(fragment); + auto unpacked = fragment; + unpacked.context.pixel->targetExportPacking.fill(ShaderRecompiler::ColorExportPacking::None); + const auto plain = materialize(unpacked); + Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId && plain->variantId == pixelResult->variantId, "the draw did not use the registered pixel shader"); + Require(plain->PipelineVariantId() != pixelResult->PipelineVariantId() && materialize(fragment)->PipelineVariantId() == pixelResult->PipelineVariantId(), "the 4_4_4_4 unorm packing did not key its own specialization"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector ReadBack(AgcDriver::VulkanDevice& device, const Block& block) { + AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back"); + device.WaitIdle(); + std::vector stored(block.bytes / 2u); + AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1); + return stored; +} + +void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool inside = x < ScissorWidth; + const std::uint32_t expected = inside ? word : before[index]; + if (stored[index] == expected) continue; + char detail[160]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x", x, y, inside ? "inside" : "outside", stored[index], expected); + throw std::runtime_error(what + detail); + } + } +} + +std::vector Fill(Block& block) { + std::vector words(block.bytes / 2u); + for (std::size_t index = 0; index < words.size(); ++index) words[index] = static_cast((index * 0x9e3779b9u) >> 11u); + std::memcpy(block.data, words.data(), block.bytes); + return words; +} + +std::string Describe(std::array rgba) { + char text[112]; + std::snprintf(text, sizeof(text), "(%.9g, %.9g, %.9g, %.9g)", rgba[0], rgba[1], rgba[2], rgba[3]); + return text; +} + +void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, const char* kind) { + const ColorTargetLayout layout(Width, Height, mode, 2); + Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout"); + const auto color = DecodeTarget(block, attrib3); + auto before = Fill(block); + const float nan = std::numeric_limits::quiet_NaN(); + const float infinity = std::numeric_limits::infinity(); + const std::array float32Cases{{ + {{1.0f, 0.0f, 0.0f, 0.0f}, 0xf000u}, + {{0.0f, 1.0f, 0.0f, 0.0f}, 0x0f00u}, + {{0.0f, 0.0f, 1.0f, 0.0f}, 0x00f0u}, + {{0.0f, 0.0f, 0.0f, 1.0f}, 0x000fu}, + {{1.0f, 1.0f, 1.0f, 1.0f}, 0xffffu}, + {{1.0f, 0.5f, 0.0f, 0.25f}, 0xf804u}, + {{0.0f, 1.0f, 0.5f, 0.25f}, 0x0f84u}, + {{nan, -1.0f, 2.0f, infinity}, 0x00ffu}, + {{1.0f / 15.0f, 2.0f / 15.0f, 7.0f / 15.0f, 14.0f / 15.0f}, 0x127eu}, + {{0.1f, 0.7f, 0.9f, 0.3f}, 0x2ad5u}, + {{-infinity, std::numeric_limits::min(), 0.999999940f, std::numeric_limits::denorm_min()}, 0x00f0u}, + }}; + for (const auto& golden : float32Cases) { + Draw(device, color, Float32Export, Float32Program(golden.rgba)); + ExpectSurface(device, block, layout, before, golden.word, std::string("a 32_ABGR export of ") + Describe(golden.rgba) + " into a " + kind + " 4_4_4_4 unorm target"); + before = ReadBack(device, block); + } + const std::array float16Cases{{ + {{1.0f, 0.0f, 0.0f, 0.0f}, 0xf000u}, + {{0.0f, 1.0f, 0.0f, 0.0f}, 0x0f00u}, + {{0.0f, 0.0f, 1.0f, 0.0f}, 0x00f0u}, + {{0.0f, 0.0f, 0.0f, 1.0f}, 0x000fu}, + {{1.0f, 1.0f, 1.0f, 1.0f}, 0xffffu}, + {{1.0f, 0.5f, 0.0f, 0.25f}, 0xf804u}, + {{0.5f, 0.75f, 0.125f, 0.25f}, 0x8b24u}, + }}; + for (const auto& golden : float16Cases) { + Draw(device, color, Float16Export, Float16Program(golden.rgba)); + ExpectSurface(device, block, layout, before, golden.word, std::string("an FP16_ABGR export of ") + Describe(golden.rgba) + " into a " + kind + " 4_4_4_4 unorm target"); + before = ReadBack(device, block); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes()); + TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, "linear"); + std::puts("4_4_4_4 unorm color target tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 27c00049911d9cb0b8b6f8de33790a4fc36faafb Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:45:21 +0200 Subject: [PATCH 045/318] fix(agc): merge kill shaders under late tests when the draw writes no depth or stencil A packed 4_4_4_4 partial write merges inside the pixel shader after early depth and stencil tests. Moving the tests ahead of a pixel kill only changes the result when the draw writes depth or stencil, so the target merge now carries whether the draw does (depth write, or stencil with a write mask and a non-keep operation), and the late-test kill refusal applies only then. The flag is part of the static shader key, the serialized request (now version 40) and the pixel input info. Co-Authored-By: Claude Sonnet 5.5 --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../Graphics/include/State.hpp | 1 + .../libSceAgcDriver/Graphics/src/State.cpp | 7 +++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 49 ++++++++++++++++++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 10 ++-- .../execution/PackedUnorm4444PartialWrite.cpp | 39 ++++++++++++--- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 2 + .../Optimization/ShaderStageInputInfo.hpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 4 +- .../src/ShaderInputInfoBuilder.cpp | 1 + .../tests/RequestSerializerTests.cpp | 6 +-- 13 files changed, 107 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 6cbf22ec75..84a83584ca 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -78,6 +78,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index dbd451d6a6..69802b363a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -171,6 +171,7 @@ VkExtent2D SampleLocationGrid(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask); +bool DepthStencilWrites(const State& state); ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); bool PartialSampleMask(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c6368dc363..c83ef04670 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1529,6 +1529,12 @@ std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t expor return mask; } +bool DepthStencilWrites(const State& state) { + if (!state.depth) return false; + const auto writes = [](const VkStencilOpState& face) { return (face.writeMask & 0xffu) != 0 && (face.failOp != VK_STENCIL_OP_KEEP || face.passOp != VK_STENCIL_OP_KEEP || face.depthFailOp != VK_STENCIL_OP_KEEP); }; + return state.depthWrite || (state.stencilTest && (writes(state.stencilFront) || writes(state.stencilBack))); +} + ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { ShaderRecompiler::TargetMerge merge; for (const auto& color : state.colors) { @@ -1538,6 +1544,7 @@ ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { merge.mask = color.partialWriteMask; merge.samples = static_cast(color.samples); merge.descriptor = EncodeTextureResource(SurfaceForTarget(color)); + merge.depthStencilWrites = DepthStencilWrites(state); } return merge; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 59d70fd4a6..c7bc673276 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8813,7 +8813,38 @@ void unorm4_4_4_4TargetTests() { const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); const auto expected = AgcDriver::Graphics::SurfaceForTarget(partial.color); const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); - Require(merge.target == 0u && merge.mask == wordMask && merge.samples == 1u && surface.baseAddress == expected.baseAddress && surface.pipeBankXor == expected.pipeBankXor && surface.width == expected.width && surface.height == expected.height && surface.format == expected.format && surface.tileMode == expected.tileMode && surface.dimension == AgcDriver::Graphics::TextureDimension::k2D && surface.mipCount == 1u && surface.samples == 1u, "the merge descriptor of CB_TARGET_MASK " + std::to_string(targetMask) + " does not decode to the target's surface"); + Require(merge.target == 0u && merge.mask == wordMask && merge.samples == 1u && !merge.depthStencilWrites && surface.baseAddress == expected.baseAddress && surface.pipeBankXor == expected.pipeBankXor && surface.width == expected.width && surface.height == expected.height && surface.format == expected.format && surface.tileMode == expected.tileMode && surface.dimension == AgcDriver::Graphics::TextureDimension::k2D && surface.mipCount == 1u && surface.samples == 1u, "the merge descriptor of CB_TARGET_MASK " + std::to_string(targetMask) + " does not decode to the target's surface"); + } + { + auto depthQueue = queue; + depthQueue.context[0x8e] = 1; + depthQueue.context[0x000] = 0; + depthQueue.context[0x002] = 0; + depthQueue.context[0x007] = (1u << 16u) | 3u; + depthQueue.context[0x00a] = 7; + depthQueue.context[0x00b] = 0; + depthQueue.context[0x010] = 0x22900983; + depthQueue.context[0x011] = 0x20000181; + for (const auto offset : {0x012u, 0x014u}) depthQueue.context[offset] = 0x100; + for (const auto offset : {0x013u, 0x015u}) depthQueue.context[offset] = 0x200; + depthQueue.context[0x005] = 0x300; + depthQueue.context[0x01e] = 0x1; + struct DepthStencilCase { + std::uint32_t control; + std::uint32_t ops; + std::uint32_t masks; + bool writes; + }; + for (const auto& [control, ops, masks, writes] : {DepthStencilCase{0x00700710u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700712u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700714u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700716u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0u, 0x01ffff01u, false}, DepthStencilCase{0x00700711u, 0x00050050u, 0x0100ff01u, false}, DepthStencilCase{0x00700713u, 0u, 0x01ffff01u, false}}) { + depthQueue.context[0x200] = control; + depthQueue.context[0x10b] = ops; + depthQueue.context[0x10c] = depthQueue.context[0x10d] = masks; + const auto what = "DB_DEPTH_CONTROL " + std::to_string(control) + " with stencil operations " + std::to_string(ops) + " and masks " + std::to_string(masks); + Require(AgcDriver::Graphics::DrawRejection(depthQueue, false).empty(), "the draw precheck refused a partial 4_4_4_4 write under " + what); + const auto partial = AgcDriver::Graphics::DecodeState(depthQueue); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); + Require(partial.depth.has_value() == ((control & 3u) != 0u) && merge.mask == 0xf000u && AgcDriver::Graphics::DepthStencilWrites(partial) == writes && merge.depthStencilWrites == writes, "a partial 4_4_4_4 write under " + what + (writes ? " did not report" : " reported") + " depth or stencil writes"); + } } { changed.context[0x8e] = 1; @@ -8871,7 +8902,21 @@ void unorm4_4_4_4TargetTests() { auto killing = merging; killing.context.pixel->pixelKillEnable = true; killing.context.pixel->earlyZ = false; - expectFailure([&] { static_cast(ShaderRecompiler::Recompile(killing)); }, "kills pixels under late tests"); + for (const bool registration : {false, true}) { + killing.target.registrationModules = registration; + const auto words = ShaderRecompiler::Recompile(killing).spirv.Copy(); + std::size_t reads = 0, writes = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + reads += static_cast(words[at] & 0xffffu) == spv::OpImageRead ? 1u : 0u; + writes += static_cast(words[at] & 0xffffu) == spv::OpImageWrite ? 1u : 0u; + } + Require(reads == 1u && writes == 1u && interlockShape(words) == "capability extension ordered begin end " && declaresEarlyFragmentTests(killing), "a killing 4_4_4_4 merge module under late tests of a draw without depth or stencil writes did not merge inside a pixel interlock after early tests"); + } + killing.context.pixel->targetMerge.depthStencilWrites = true; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(killing)); }, "kills pixels under late tests of a draw writing depth or stencil"); + auto earlyKilling = killing; + earlyKilling.context.pixel->earlyZ = true; + static_cast(ShaderRecompiler::Recompile(earlyKilling)); auto exporting = merging; exporting.context.pixel->depthExportEnable = true; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(exporting)); }, "exports depth"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index bce4131fdc..99da8fb457 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4831,6 +4831,10 @@ void verifyPixelRequestSerialization() { movedMerge.context.pixel->targetMerge.descriptor[0] = 0x200u; RecompileCacheKey::Build(movedMerge, replayKey); require(key == replayKey, "the merged target's descriptor changed the static shader identity"); + auto depthWritingMerge = request; + depthWritingMerge.context.pixel->targetMerge.depthStencilWrites = true; + RecompileCacheKey::Build(depthWritingMerge, replayKey); + require(key != replayKey, "the merge draw's depth or stencil writes did not change the static shader identity"); auto withoutMerge = request; withoutMerge.context.pixel->targetMerge = {}; RecompileCacheKey::Build(withoutMerge, replayKey); @@ -5114,12 +5118,12 @@ void verifyVertexInfoVersion19() { require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks"); constexpr std::size_t iterationBytes = 35u; - constexpr std::size_t mergeBytes = 38u; + constexpr std::size_t mergeBytes = 39u; const auto mergeOffset = static_cast(blendOffset + 4u + iterationBytes); auto merging = pixel; - merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}}; + merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true}; const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); - require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 38 bytes the version 40 pixel info adds after the pixel iteration fields"); + require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 39 bytes the version 40 pixel info adds after the pixel iteration fields"); require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); const auto withoutMerge = [&](std::vector bytes) { bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast(mergeBytes)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 73696717be..47df17bf42 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -47,6 +47,9 @@ constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t Float32Export = 9; constexpr std::uint32_t Float16Export = 4; +constexpr std::uint32_t PositionInputs = 0x300u; +constexpr std::uint32_t LateKill = 0x40u; +constexpr std::array CheckerboardKill{0x7e040f00u, 0x7e060f01u, 0x3a040702u, 0x36040481u, 0x7da40480u}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -77,10 +80,13 @@ std::uint32_t Half(float value) { return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); } -std::span Program(const std::array& vgprs, bool compressed) { +std::span Program(const std::array& vgprs, bool compressed, bool killing = false) { Require(programCount < Programs.size(), "too many test programs"); auto& code = Programs[programCount++]; std::size_t at = 0; + if (killing) { + for (const auto word : CheckerboardKill) code[at++] = word; + } for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); code[at++] = vgprs[vgpr]; @@ -91,8 +97,8 @@ std::span Program(const std::array& vgprs return std::span(code.data(), at); } -std::span Float32Program(std::array rgba) { - return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false); +std::span Float32Program(std::array rgba, bool killing = false) { + return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false, killing); } std::span Float16Program(std::array rgba) { @@ -171,7 +177,7 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { return color; } -std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode) { +std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode, bool killing = false) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); @@ -210,7 +216,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa state.blendConstants = {}; const auto codeAddress = reinterpret_cast(pixelCode.data()); - const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}}; + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, killing ? PositionInputs : 0u}, {0x1b4, killing ? PositionInputs : 0u}, {0x203, killing ? LateKill : 0u}, {0x1c5, exportFormat}}; const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, @@ -225,6 +231,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); + Require(fragment.context.pixel->pixelKillEnable == killing && !fragment.context.pixel->earlyZ && !fragment.context.pixel->targetMerge.depthStencilWrites, "the draw's pixel kill under late tests or its depth and stencil writes did not decode"); Require((fragment.context.pixel->targetMerge.mask != 0u) == partial && fragment.context.pixel->targetMerge.mask == color.partialWriteMask, "the draw's target merge disagrees with its write mask"); const ShaderRecompiler::SrtRuntime runtime{}; const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); @@ -300,6 +307,21 @@ void BackToBack(AgcDriver::VulkanDevice& device, Block& block, const ColorTarget } } +void ExpectKilled(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + const bool kept = x < ScissorWidth && ((x ^ y) & 1u) == 0u; + const std::uint32_t expected = kept ? (before[index] & ~wordMask) | (word & wordMask) : before[index]; + if (stored[index] == expected) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": %s pixel (%u, %u) holds 0x%04x, expected 0x%04x (it held 0x%04x)", kept ? "a surviving" : x < ScissorWidth ? "a killed" : "an unscissored", x, y, stored[index], expected, before[index]); + throw std::runtime_error(what + detail); + } + } +} + std::string Describe(std::uint32_t mask, std::array rgba) { char text[160]; std::snprintf(text, sizeof(text), "CB_TARGET_MASK 0x%x write of (%.9g, %.9g, %.9g, %.9g)", mask, rgba[0], rgba[1], rgba[2], rgba[3]); @@ -350,7 +372,12 @@ int main() { const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves)); ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); - std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws and 3 back-to-back sequences merged into a %ux%u tiled target, the pixels outside the scissor kept\n", cases.size() + 1u, Width, Height); + for (const auto& [mask, rgba] : {Case{0x1u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { + before = Fill(block, 91u + mask); + const auto killedMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba, true), true); + ExpectKilled(*device, block, layout, before, Packed(rgba), killedMask, "a late-test checkerboard-killing " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + } + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences and 2 checkerboard-killing draws merged into a %ux%u tiled target, the killed pixels and those outside the scissor kept\n", cases.size() + 1u, Width, Height); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 34f15975af..fea7c67b40 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -220,6 +220,7 @@ private: append(key, value.targetMerge.target); append(key, value.targetMerge.mask); append(key, value.targetMerge.samples); + append(key, value.targetMerge.depthStencilWrites); } static void append(std::vector& key, const ShaderVertexResourceDestination& value) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 73fd3d1ce0..389ef2f0f3 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -359,6 +359,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU32(info.targetMerge.mask); writer.WriteU8(info.targetMerge.samples); for (const std::uint32_t word : info.targetMerge.descriptor) writer.WriteU32(word); + writer.WriteBool(info.targetMerge.depthStencilWrites); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -434,6 +435,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { info.targetMerge.mask = static_cast(mask); info.targetMerge.samples = reader.ReadU8(); for (std::uint32_t& word : info.targetMerge.descriptor) word = reader.ReadU32(); + info.targetMerge.depthStencilWrites = reader.ReadBool(); } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 99f08344b9..4e271d4660 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -198,6 +198,7 @@ struct ShaderPixelInputInfo { std::uint8_t mergeTarget = 0; std::uint16_t mergeMask = 0; std::uint8_t mergeSamples = 1; + bool mergeDepthStencilWrites = false; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 9db85f8311..7c56960968 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -156,6 +156,7 @@ struct TargetMerge { std::uint16_t mask = 0; std::uint8_t samples = 1; std::array descriptor{}; + bool depthStencilWrites = false; bool operator==(const TargetMerge&) const = default; }; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 697a656367..1110470d17 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -384,8 +384,8 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeStencilRefReplacingEXT); } const bool merge = pixel.mergeMask != 0u; - if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ))) { - throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); + if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ && pixel.mergeDepthStencilWrites))) { + throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); } if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 98b1fb91ee..6161de1552 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -159,6 +159,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.mergeTarget = pixel.targetMerge.target; pixelStorage.mergeMask = pixel.targetMerge.mask; pixelStorage.mergeSamples = pixel.targetMerge.samples; + pixelStorage.mergeDepthStencilWrites = pixel.targetMerge.depthStencilWrites; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index b3482d7888..00f6495be8 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -54,7 +54,7 @@ void InsertMeshBarycentricFlag(std::vector& bytes) { void InsertPixelDefaultsAfterFrontFace(std::vector& bytes) { bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u}); - bytes.insert(bytes.begin() + 282u, 73u, 0u); + bytes.insert(bytes.begin() + 282u, 74u, 0u); bytes[322] = 1u; } @@ -411,7 +411,7 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38); auto upgraded = main38; - if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 355u); + if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 356u); upgraded[4] = 40u; if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); @@ -500,7 +500,7 @@ int main() { auto version35 = DecodeBase64(serializer.Serialize(request)); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); Require(stencilOperationValue < version35.end() && *stencilOperationValue == 0u); - version35.erase(stencilOperationValue, stencilOperationValue + 1 + 38); + version35.erase(stencilOperationValue, stencilOperationValue + 1 + 39); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); From acdbc02c8fcc3557bf7e694be343bd42e122cd9e Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:15:23 +0200 Subject: [PATCH 046/318] docs(agc): note that CB resolves of 4_4_4_4 color targets throw A CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e1dbad279e..ef2ea9da23 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -160,6 +160,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. +- [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. From 8b37d049abb96a386f44c886043838e6fa97c0ab Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:53:16 +0200 Subject: [PATCH 047/318] fix(agc): write a merged 4_4_4_4 word only for pixels that survive the shader The merge used to write the target at the color export, so a pixel killed later by a valid-mask export still changed its word. The merged word is now kept until the shader returns and written inside the pixel interlock only when the pixel's valid mask is still set. The execution test kills a checkerboard both before and after the merging export. Co-Authored-By: Claude Sonnet 5.5 --- .../execution/PackedUnorm4444PartialWrite.cpp | 34 +++++++++---- .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 2 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 1 + .../SpirvBackend/src/SpirvModuleEmitter.cpp | 50 ++++++++++++++++--- 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 47df17bf42..7ec9170878 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -51,6 +51,8 @@ constexpr std::uint32_t PositionInputs = 0x300u; constexpr std::uint32_t LateKill = 0x40u; constexpr std::array CheckerboardKill{0x7e040f00u, 0x7e060f01u, 0x3a040702u, 0x36040481u, 0x7da40480u}; +enum class Kill { None, BeforeExport, AfterExport }; + alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; @@ -80,25 +82,33 @@ std::uint32_t Half(float value) { return ((((bits >> 23u) & 0xffu) - 112u) << 10u) | ((bits >> 13u) & 0x3ffu); } -std::span Program(const std::array& vgprs, bool compressed, bool killing = false) { +std::span Program(const std::array& vgprs, bool compressed, Kill kill = Kill::None) { Require(programCount < Programs.size(), "too many test programs"); auto& code = Programs[programCount++]; std::size_t at = 0; - if (killing) { + if (kill == Kill::BeforeExport) { for (const auto word : CheckerboardKill) code[at++] = word; } for (std::uint32_t vgpr = 0; vgpr < (compressed ? 2u : 4u); ++vgpr) { code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); code[at++] = vgprs[vgpr]; } - code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; - code[at++] = compressed ? 0x00000504u : 0x07060504u; + if (kill == Kill::AfterExport) { + code[at++] = compressed ? 0xf800040fu : 0xf800000fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + for (const auto word : CheckerboardKill) code[at++] = word; + code[at++] = 0xf8001890u; + code[at++] = 0u; + } else { + code[at++] = compressed ? 0xf8001c0fu : 0xf800180fu; + code[at++] = compressed ? 0x00000504u : 0x07060504u; + } code[at++] = 0xbf810000u; return std::span(code.data(), at); } -std::span Float32Program(std::array rgba, bool killing = false) { - return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false, killing); +std::span Float32Program(std::array rgba, Kill kill = Kill::None) { + return Program({std::bit_cast(rgba[0]), std::bit_cast(rgba[1]), std::bit_cast(rgba[2]), std::bit_cast(rgba[3])}, false, kill); } std::span Float16Program(std::array rgba) { @@ -372,12 +382,14 @@ int main() { const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves)); ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); - for (const auto& [mask, rgba] : {Case{0x1u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { - before = Fill(block, 91u + mask); - const auto killedMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba, true), true); - ExpectKilled(*device, block, layout, before, Packed(rgba), killedMask, "a late-test checkerboard-killing " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + for (const auto kill : {Kill::BeforeExport, Kill::AfterExport}) { + for (const auto& [mask, rgba] : {Case{0x1u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { + before = Fill(block, 91u + mask); + const auto killedMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba, kill), true); + ExpectKilled(*device, block, layout, before, Packed(rgba), killedMask, std::string(kill == Kill::BeforeExport ? "a late-test checkerboard kill before " : "a late-test checkerboard kill after ") + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + } } - std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences and 2 checkerboard-killing draws merged into a %ux%u tiled target, the killed pixels and those outside the scissor kept\n", cases.size() + 1u, Width, Height); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences and 4 checkerboard-killing draws merged into a %ux%u tiled target, the killed pixels and those outside the scissor kept\n", cases.size() + 1u, Width, Height); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index c210de9ddd..4dde3cba3e 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -101,6 +101,7 @@ DppTargetLane EmitDppMirrorTargetLane(SpirvEmitterState& state, std::uint32_t su DppTargetLane EmitDppTargetLane(SpirvEmitterState& state, std::uint32_t control); std::uint32_t InputVariableForKind(const SpirvEmitterState& state, StageInputKind kind); bool OrderedPixelShader(const SpirvEmitterState& state); +void FlushColorTargetMerge(SpirvEmitterState& state); const SpirvInputBinding* SpirvInputBindingForParameter(const SpirvEmitterState& state, std::uint32_t location); std::uint32_t EmitVertexParameterComponentU32(SpirvEmitterState& state, const SpirvInputBinding& input, std::uint32_t component); std::uint32_t EmitInputComponentU32(SpirvEmitterState& state, StageInputKind kind, std::uint32_t component); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 3603bb59b8..953f8330b5 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -217,6 +217,8 @@ struct SpirvEmitterState { std::uint32_t currentLabel = 0; const IrBlock* currentBlock = nullptr; std::uint32_t pixelValidMaskVariable = 0; + std::uint32_t mergeWordVariable = 0; + std::uint32_t mergePendingVariable = 0; std::uint32_t quadPixelUncovered = 0; std::uint32_t sampleMaskInputVariable = 0; std::uint32_t quadSampleCoverage = 0; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index fe330d906a..59824a75cd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -90,6 +90,7 @@ void EmitReturnTerminator(SpirvValueEmitContext& ctx) { RecordBdaFaultWords(state, ConstantU32(state, static_cast(hash)), ConstantU32(state, static_cast(hash >> 32u)), ConstantU32(state, state.loopGuardLimit), EmitBinaryU32(state, spv::OpISub, pc, ConstantU32(state, 1u)), BdaAbi::FaultReason::LoopLimit); }); } + FlushColorTargetMerge(state); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index f657f4122a..c88b3f34ad 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -414,26 +414,35 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target return result; } -void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) { - const auto& pixel = PixelInfo(state); +struct MergeTexel { + std::uint32_t resource; + std::uint32_t coord; +}; + +MergeTexel MergeTargetTexel(SpirvEmitterState& state) { const auto& images = state.program.Info().images; const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); if (image == images.end()) FailEmit("the color target merge image was not collected"); if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u) FailEmit("the color target merge inputs were not declared"); - const auto resource = static_cast(image - images.begin()); + if (state.mergeWordVariable == 0u || state.mergePendingVariable == 0u) FailEmit("the color target merge variables were not declared"); const auto position = [&](std::uint32_t component) { return Unary(state, spv::OpConvertFToU, TypeU32(state), Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component))); }; const auto x = position(0u); const auto y = position(1u); const auto coord = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2u), coord, x, y); + return {static_cast(image - images.begin()), coord}; +} + +void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) { + const auto& pixel = PixelInfo(state); + const auto [resource, coord] = MergeTargetTexel(state); const auto stored = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord), 0u); const auto word = PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color); const auto kept = EmitBinaryU32(state, spv::OpBitwiseAnd, stored, ConstantU32(state, ~static_cast(pixel.mergeMask))); const auto merged = EmitOrU32(state, kept, EmitBinaryU32(state, spv::OpBitwiseAnd, word, ConstantU32(state, pixel.mergeMask))); - const auto texel = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), texel, merged, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)); - EmitStorageImageWrite(state, resource, 0u, coord, texel, 0u); + state.module.AddFunction(spv::OpStore, state.mergeWordVariable, merged); + state.module.AddFunction(spv::OpStore, state.mergePendingVariable, ConstantU32(state, 1u)); } bool AlphaToCoverage(const SpirvEmitterState& state) { @@ -739,6 +748,26 @@ std::uint32_t ConvertPositionToClipSpace(SpirvEmitterState& state, std::uint32_t } +void FlushColorTargetMerge(SpirvEmitterState& state) { + if (state.mergePendingVariable == 0u) return; + const auto pending = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), pending, state.mergePendingVariable); + auto write = Binary(state, spv::OpINotEqual, TypeBool(state), pending, ConstantU32(state, 0u)); + if (state.pixelValidMaskVariable != 0u) { + const auto valid = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), valid, state.pixelValidMaskVariable); + write = Binary(state, spv::OpLogicalAnd, TypeBool(state), write, Binary(state, spv::OpINotEqual, TypeBool(state), valid, ConstantU32(state, 0u))); + } + EmitIfCondition(state, write, [&] { + const auto [resource, coord] = MergeTargetTexel(state); + const auto merged = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), merged, state.mergeWordVariable); + const auto texel = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), texel, merged, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)); + EmitStorageImageWrite(state, resource, 0u, coord, texel, 0u); + }); +} + namespace { std::uint32_t MeshArray(SpirvEmitterState& state, std::uint32_t storage, std::uint32_t type, std::uint32_t count) { @@ -1460,6 +1489,10 @@ void EmitProgram(SpirvEmitterState& state) { state.pixelValidMaskVariable = state.module.AllocateId(); state.module.AddName(state.pixelValidMaskVariable, "pixel_valid_mask_active"); } + if (state.program.Resources().stage == IrShaderStage::Pixel && state.inputInfo.pixel != nullptr && state.inputInfo.pixel->mergeMask != 0u) { + state.mergeWordVariable = state.module.AllocateId(); + state.mergePendingVariable = state.module.AllocateId(); + } for (const IrBlock* block : program.BlockOrder()) { const auto label = state.module.AllocateId(); state.labels.emplace(block, label); @@ -1508,6 +1541,10 @@ void EmitProgram(SpirvEmitterState& state) { if (state.pixelValidMaskVariable != 0u) { state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.pixelValidMaskVariable, spv::StorageClassFunction); } + if (state.mergePendingVariable != 0u) { + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.mergeWordVariable, spv::StorageClassFunction); + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), state.mergePendingVariable, spv::StorageClassFunction, ConstantU32(state, 0u)); + } for (std::uint32_t half = 0; half < state.laneCount; half++) { auto& lane = half == 0u ? ctx : high; if (lane.scratchU32Variable != 0u) { @@ -1582,6 +1619,7 @@ void EmitProgram(SpirvEmitterState& state) { if (state.quadSampleCoverage != 0u) StoreCoverage(state, ConstantI32(state, -1)); } if (program.BlockOrder().empty()) { + FlushColorTargetMerge(state); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpEndInvocationInterlockEXT); } From 8d6d86405399dfa46d2e800e299f2d3ee2438253 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:23:49 +0200 Subject: [PATCH 048/318] fix(agc): leave the merged 4_4_4_4 target out of the draw's attachments A merging draw read and wrote its packed target as a storage image while the same image stayed a color attachment of the pass, a feedback loop that NVIDIA only tolerates. The merged slot is now VK_ATTACHMENT_UNUSED in the subpass and VK_FORMAT_UNDEFINED in the rendering formats, its view is left out of the render pass and VkFramebuffer, and dynamic rendering binds no view there. The target stays in the draw's tracked and dirty images. Pipelines and continued passes are keyed on the merged slot. Co-Authored-By: Claude Sonnet 5.5 --- .../Graphics/include/Pipeline.hpp | 2 ++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 3 +++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 27 ++++++++++++++----- .../Graphics/src/PipelineJournal.cpp | 5 +++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++ 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index 61679c4f59..6c3a04813b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -83,6 +83,8 @@ private: VkPipeline secondChannel = VK_NULL_HANDLE; std::size_t attachments = 0; std::size_t colorAttachments = 0; + std::vector boundAttachments; + std::uint32_t boundColorAttachments = 0; bool depthBounds = false; bool depthBias = false; bool libraries = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 3c3115e9b5..52ef639841 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -50,6 +50,9 @@ std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std: mix(state.blends.size()); for (const auto& color : state.colors) mix(color.exportIndex); } + for (const auto& color : state.colors) { + if (color.partialWriteMask != 0u) mix(0x6d65726765000000ull | color.exportIndex); + } mix(state.renderExtent.width); mix(state.renderExtent.height); mix(state.samples); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index eaca4be3f4..f7bef401dc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -278,6 +278,7 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& for (const auto& color : state.colors) { appendKey(keys.renderPass, color.format); appendKey(keys.renderPass, color.exportIndex); + appendKey(keys.renderPass, color.partialWriteMask != 0u); } appendKey(keys.renderPass, state.depth.has_value()); if (state.depth) appendKey(keys.renderPass, state.depth->format); @@ -314,6 +315,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias), dynamicRendering(context.graphicsPipelineLibrary) { PerformanceTimer timing("Vulkan.GraphicsPipeline"); + for (const auto& color : state.colors) boundAttachments.push_back(color.partialWriteMask == 0u); + if (state.depth) boundAttachments.push_back(true); sampleLocationsEnabled = ValidateSampleLocations(context, state); sampleLocationGrid = hostSampleLocationGrid(context, state); // A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep @@ -321,7 +324,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput this->context.bufferPool.reset(); Require(state.blends.size() == (state.colors.empty() ? 0u : state.colors.back().exportIndex + 1u) && state.colors.size() <= state.blends.size(), "blend states do not match decoded color state"); Require(state.blends.size() <= context.limits.maxColorAttachments, "color targets exceed device attachment limits"); - Require(state.hasColorTarget || (context.limits.framebufferNoAttachmentsSampleCounts & static_cast(state.samples)) != 0, "device does not support rendering without attachments at the draw's sample count"); + const bool boundColor = std::ranges::any_of(state.colors, [](const ColorTarget& color) { return color.partialWriteMask == 0u; }); + Require((state.hasColorTarget && (boundColor || state.colors.empty() || state.depth.has_value())) || (context.limits.framebufferNoAttachmentsSampleCounts & static_cast(state.samples)) != 0, "device does not support rendering without attachments at the draw's sample count"); Require(state.samples == 1 || !state.depth.has_value() || (context.limits.framebufferDepthSampleCounts & static_cast(state.samples)) != 0, "device does not support depth targets with the draw's sample count"); Require(!depthBounds || context.depthBounds, "device does not support the depth bounds test"); Require(!depthBias || state.depthBiasClamp == 0.0f || context.depthBiasClamp, "device does not support depth bias clamping"); @@ -393,6 +397,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput std::vector colors; std::vector references(state.blends.size(), VkAttachmentReference{VK_ATTACHMENT_UNUSED, attachmentLayout}); for (std::uint32_t index = 0; index < state.colors.size(); ++index) { + if (state.colors[index].partialWriteMask != 0u) continue; VkAttachmentDescription color{}; color.format = state.colors[index].format; color.samples = state.mergedCoverage ? VK_SAMPLE_COUNT_1_BIT : static_cast(state.samples); @@ -402,9 +407,10 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; color.initialLayout = attachmentLayout; color.finalLayout = attachmentLayout; + references.at(state.colors[index].exportIndex) = {static_cast(colors.size()), attachmentLayout}; colors.push_back(color); - references.at(state.colors[index].exportIndex) = {index, attachmentLayout}; } + boundColorAttachments = static_cast(colors.size()); VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; subpass.colorAttachmentCount = static_cast(references.size()); @@ -424,7 +430,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput subpass.pDepthStencilAttachment = &depthReference; } std::vector renderingColors(state.blends.size(), VK_FORMAT_UNDEFINED); - for (const auto& color : state.colors) renderingColors.at(color.exportIndex) = color.format; + for (const auto& color : state.colors) { + if (color.partialWriteMask == 0u) renderingColors.at(color.exportIndex) = color.format; + } VkPipelineRenderingCreateInfoKHR rendering{VK_STRUCTURE_TYPE_PIPELINE_RENDERING_CREATE_INFO_KHR}; rendering.colorAttachmentCount = static_cast(renderingColors.size()); rendering.pColorAttachmentFormats = renderingColors.empty() ? nullptr : renderingColors.data(); @@ -440,7 +448,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput if (dynamicRendering) { depthFormat = state.depth ? HostDepthFormat(context, state.depth->format) : VK_FORMAT_UNDEFINED; colorSlots.assign(state.blends.size(), ~0u); - for (std::uint32_t index = 0; index < state.colors.size(); ++index) colorSlots.at(state.colors[index].exportIndex) = index; + for (std::uint32_t index = 0; index < state.colors.size(); ++index) { + if (state.colors[index].partialWriteMask == 0u) colorSlots.at(state.colors[index].exportIndex) = index; + } this->attachmentLayout = attachmentLayout; } VkRenderPassCreateInfo passInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; @@ -684,7 +694,11 @@ std::shared_ptr Pipeline::AcquireFramebuffer(std::span(context, renderPass, targets, extent, layers); + std::vector boundViews; + for (std::size_t i = 0; i < targets.size(); ++i) { + if (boundAttachments[i]) boundViews.push_back(targets[i]); + } + auto framebuffer = std::make_shared(context, renderPass, renderPass == VK_NULL_HANDLE ? targets : std::span(boundViews), extent, layers); if (!resident) return framebuffer; // Beyond the bound the least recently used unreferenced entry goes. constexpr std::size_t bound = 8; @@ -742,7 +756,7 @@ void Pipeline::Begin(VkCommandBuffer commands, const Framebuffer& framebuffer, V const auto locations = SampleLocations(state); const auto grid = sampleLocationGrid; const VkSampleLocationsInfoEXT info{VK_STRUCTURE_TYPE_SAMPLE_LOCATIONS_INFO_EXT, nullptr, static_cast(state.samples), grid, state.samples * grid.width * grid.height, locations.data()}; - const VkAttachmentSampleLocationsEXT initial{static_cast(state.colors.size()), info}; + const VkAttachmentSampleLocationsEXT initial{boundColorAttachments, info}; const VkSubpassSampleLocationsEXT post{0, info}; VkRenderPassSampleLocationsBeginInfoEXT sampleLocations{VK_STRUCTURE_TYPE_RENDER_PASS_SAMPLE_LOCATIONS_BEGIN_INFO_EXT}; sampleLocations.attachmentInitialSampleLocationsCount = state.depth ? 1 : 0; @@ -891,6 +905,7 @@ void pipelineKey(std::vector& key, const Context& context, const Stat for (const auto& color : state.colors) { append(key, color.format); append(key, color.componentMapping); + append(key, color.partialWriteMask != 0u); } if (state.blends.size() != state.colors.size()) { for (const auto& color : state.colors) append(key, color.exportIndex); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp index 886a63f60b..f78f97d4ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp @@ -44,7 +44,7 @@ static_assert(sizeof(ShaderRecompiler::TessellationConfiguration) == 40, "Tessel constexpr std::uint32_t FileMagic = 0x4c4a5041u; // Bumped when the recipe codec, CanonicalPipelineState or the store key change in a way the // layout guards above do not catch (a field renumbered or reinterpreted at the same size). -constexpr std::uint32_t FileFormat = 4u; +constexpr std::uint32_t FileFormat = 5u; constexpr std::uint32_t ModuleRecord = 1u; constexpr std::uint32_t GraphicsRecord = 2u; constexpr std::uint32_t ComputeRecord = 3u; @@ -195,6 +195,7 @@ void encodeState(Writer& writer, const State& state) { writer.U32(static_cast(color.format)); writer.U32(color.exportIndex); writer.Raw(color.componentMapping); + writer.Raw(color.partialWriteMask); } writer.Raws(std::span(state.blends)); for (const auto value : state.blendConstants) writer.Raw(value); @@ -254,6 +255,7 @@ void decodeState(Reader& reader, State& state) { color.format = static_cast(reader.U32()); color.exportIndex = reader.U32(); color.componentMapping = reader.Raw(); + color.partialWriteMask = reader.Raw(); state.colors.push_back(color); } reader.Raws(state.blends, 8u); @@ -1026,6 +1028,7 @@ State CanonicalPipelineState(const State& state) { canonicalColor.format = color.format; canonicalColor.exportIndex = color.exportIndex; canonicalColor.componentMapping = color.componentMapping; + canonicalColor.partialWriteMask = color.partialWriteMask; canonical.colors.push_back(canonicalColor); } canonical.blends = state.blends; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c7bc673276..be91393e26 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -10140,6 +10140,15 @@ void pipelineJournalTests(bool longPath = false, bool extended = false) { BuildPipelineStoreKey(mergedKey, context, mergedState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); BuildPipelineStoreKey(mergedReplayed, context, mergedDecoded.state, mergedDecoded.vertexInput, layouts.View(), shaders, mergedDecoded.attachmentLayout); Require(mergedKey != fourSamples && mergedKey == mergedReplayed, "the decoded merged coverage state keys another pipeline"); + auto partialState = state; + partialState.colors[0].partialWriteMask = 0xf000u; + GraphicsPipelineRecipe partialDecoded; + Require(DecodeGraphicsPipelineRecipe(EncodeGraphicsPipelineRecipe(MakeGraphicsPipelineRecipe(partialState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL)), partialDecoded), "the partial write recipe does not decode"); + Require(partialDecoded.state.colors.size() == partialState.colors.size() && partialDecoded.state.colors[0].partialWriteMask == 0xf000u, "the recipe dropped the merged target's partial write mask"); + std::vector partialKey, partialReplayed; + BuildPipelineStoreKey(partialKey, context, partialState, input, layouts.View(), shaders, VK_IMAGE_LAYOUT_GENERAL); + BuildPipelineStoreKey(partialReplayed, context, partialDecoded.state, partialDecoded.vertexInput, layouts.View(), shaders, partialDecoded.attachmentLayout); + Require(partialKey != original && partialKey == partialReplayed, "the decoded partial write state keys another pipeline"); auto movedState = state; movedState.viewport.x = 5; movedState.colors[0].address = 0x9000; From f3ea4b24075be447ad6d70e8fe0f3f3dd63b9702 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:52:16 +0200 Subject: [PATCH 049/318] test(agc): check 4_4_4_4 merges against fixed masks, same-draw overdraw and depth The partial write test now names the word mask of every CB_TARGET_MASK case instead of taking it from PackedWriteMask, draws 16 overlapping layers in one draw whose merged nibble must come from the last layer, and merges behind and in front of a depth target filled by a checkerboard-killing draw: a depth-rejected fragment leaves its word. Co-Authored-By: Claude Sonnet 5.5 --- .../execution/PackedUnorm4444PartialWrite.cpp | 179 ++++++++++++++---- 1 file changed, 144 insertions(+), 35 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 7ec9170878..394411849b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -48,6 +49,7 @@ constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t Float32Export = 9; constexpr std::uint32_t Float16Export = 4; constexpr std::uint32_t PositionInputs = 0x300u; +constexpr std::uint32_t DepthInput = 0x400u; constexpr std::uint32_t LateKill = 0x40u; constexpr std::array CheckerboardKill{0x7e040f00u, 0x7e060f01u, 0x3a040702u, 0x36040481u, 0x7da40480u}; @@ -115,6 +117,34 @@ std::span Float16Program(std::array rgba) { return Program({Half(rgba[0]) | (Half(rgba[1]) << 16u), Half(rgba[2]) | (Half(rgba[3]) << 16u), 0, 0}, true); } +std::span DepthAsXProgram(std::array rgba) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + code[at++] = 0x7e080300u; + for (std::uint32_t vgpr = 1; vgpr < 4u; ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = std::bit_cast(rgba[vgpr]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +const std::array, 3>& TriangleAt(float z) { + static std::map, 3>> triangles; + return triangles.try_emplace(z, std::array, 3>{{{-1.0f, -1.0f, z, 1.0f}, {3.0f, -1.0f, z, 1.0f}, {-1.0f, 3.0f, z, 1.0f}}}).first->second; +} + +struct DrawSetup { + std::uint32_t inputs = 0; + std::uint32_t shaderControl = 0; + std::span> vertices = Triangle; + std::uint64_t depth = 0; + bool depthWrite = false; +}; + struct Block { explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) { #ifdef _WIN32 @@ -187,12 +217,12 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { return color; } -std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode, bool killing = false) { +std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask, std::uint32_t exportFormat, std::span pixelCode, const DrawSetup& setup = {}) { const auto target = device.Target(); constexpr std::uint32_t waveSize = 64; std::vector vertexUserData(4, 0u); - const auto address = reinterpret_cast(Triangle.data()); - const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(Triangle.size()), 0x01016facu}; + const auto address = reinterpret_cast(setup.vertices.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(setup.vertices.size()), 0x01016facu}; std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; ShaderRecompiler::RecompileRequest vertex{ @@ -224,9 +254,15 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa state.blends = {blend}; state.blend = blend; state.blendConstants = {}; + if (setup.depth != 0) { + state.depth = AgcDriver::Graphics::DepthTarget{setup.depth, 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false}; + state.depthTest = true; + state.depthWrite = setup.depthWrite; + state.depthCompare = VK_COMPARE_OP_LESS; + } const auto codeAddress = reinterpret_cast(pixelCode.data()); - const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, killing ? PositionInputs : 0u}, {0x1b4, killing ? PositionInputs : 0u}, {0x203, killing ? LateKill : 0u}, {0x1c5, exportFormat}}; + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, setup.inputs}, {0x1b4, setup.inputs}, {0x203, setup.shaderControl}, {0x1c5, exportFormat}}; const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, @@ -241,7 +277,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); - Require(fragment.context.pixel->pixelKillEnable == killing && !fragment.context.pixel->earlyZ && !fragment.context.pixel->targetMerge.depthStencilWrites, "the draw's pixel kill under late tests or its depth and stencil writes did not decode"); + Require(fragment.context.pixel->pixelKillEnable == ((setup.shaderControl & LateKill) != 0u) && !fragment.context.pixel->earlyZ && fragment.context.pixel->targetMerge.depthStencilWrites == (partial && setup.depth != 0 && setup.depthWrite), "the draw's pixel kill under late tests or its depth and stencil writes did not decode"); Require((fragment.context.pixel->targetMerge.mask != 0u) == partial && fragment.context.pixel->targetMerge.mask == color.partialWriteMask, "the draw's target merge disagrees with its write mask"); const ShaderRecompiler::SrtRuntime runtime{}; const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); @@ -252,7 +288,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa {ShaderStage::Vertex, &vertexResult, 0}, {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} }}; - const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangle.size()), 0, 1, 0, false}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(setup.vertices.size()), 0, 1, 0, false}; device.Draw(state, draw, shaders); return color.partialWriteMask; } @@ -289,15 +325,36 @@ std::vector Fill(Block& block, std::uint32_t seed) { struct Case { std::uint32_t mask; + std::uint32_t wordMask; std::array rgba; }; +std::uint32_t DrawCase(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t mask, std::uint32_t wordMask, std::uint32_t exportFormat, std::span pixelCode, const DrawSetup& setup = {}) { + const auto drawn = Draw(device, color, mask, exportFormat, pixelCode, setup); + char detail[160]; + std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x merged the word mask 0x%04x, not 0x%04x", mask, drawn, mask == 0xfu ? 0u : wordMask); + Require(drawn == (mask == 0xfu ? 0u : wordMask), detail); + return wordMask; +} + +void ExpectWords(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& expected, const std::vector& before, const std::string& what) { + const auto stored = ReadBack(device, block); + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + const auto index = layout.Offset(x, y) / 2u; + if (stored[index] == expected[index]) continue; + char detail[192]; + std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, stored[index], expected[index], before[index]); + throw std::runtime_error(what + detail); + } + } +} + void BackToBack(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, std::span draws, std::uint32_t seed, const std::string& what) { const auto before = Fill(block, seed); auto expected = before; - for (const auto& [mask, rgba] : draws) { - const auto drawn = Draw(device, color, mask, Float32Export, Float32Program(rgba)); - const auto wordMask = mask == 0xfu ? 0xffffu : drawn; + for (const auto& [mask, wordMask, rgba] : draws) { + DrawCase(device, color, mask, wordMask, Float32Export, Float32Program(rgba)); for (std::uint32_t y = 0; y < Height; ++y) { for (std::uint32_t x = 0; x < ScissorWidth; ++x) { auto& word = expected[layout.Offset(x, y) / 2u]; @@ -305,16 +362,63 @@ void BackToBack(AgcDriver::VulkanDevice& device, Block& block, const ColorTarget } } } - const auto stored = ReadBack(device, block); + ExpectWords(device, block, layout, expected, before, what); +} + +void SameDrawOverdraw(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color, float first, float last, std::uint32_t seed) { + constexpr std::uint32_t Layers = 16; + static std::map>> stacks; + auto& vertices = stacks[last]; + for (std::uint32_t layer = 0; layer < Layers; ++layer) { + const auto& triangle = TriangleAt(layer % 2u == Layers % 2u ? first : last); + vertices.insert(vertices.end(), triangle.begin(), triangle.end()); + } + const auto before = Fill(block, seed); + auto expected = before; + const std::uint32_t nibble = static_cast(std::nearbyint(static_cast(last) * 15.0)); for (std::uint32_t y = 0; y < Height; ++y) { - for (std::uint32_t x = 0; x < Width; ++x) { - const auto index = layout.Offset(x, y) / 2u; - if (stored[index] == expected[index]) continue; - char detail[160]; - std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) holds 0x%04x, expected 0x%04x (it held 0x%04x)", x, y, stored[index], expected[index], before[index]); - throw std::runtime_error(what + detail); + for (std::uint32_t x = 0; x < ScissorWidth; ++x) { + auto& word = expected[layout.Offset(x, y) / 2u]; + word = static_cast((word & 0x0fffu) | (nibble << 12u)); } } + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, DepthAsXProgram({0.0f, 0.4f, 0.6f, 1.0f}), {DepthInput, 0u, vertices}); + char what[160]; + std::snprintf(what, sizeof(what), "one draw of %u full-screen triangles alternating z %.2f and %.2f, merging z into X", Layers, first, last); + ExpectWords(device, block, layout, expected, before, what); +} + +void DepthRejection(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color) { + Block depth(static_cast(Width) * Height * 4u); + const std::vector cleared(depth.bytes / 4u, 1.0f); + std::memcpy(depth.data, cleared.data(), depth.bytes); + const std::array nearer{0.6f, 0.2f, 0.8f, 0.4f}; + const std::array behind{0.0f, 1.0f, 1.0f, 1.0f}; + const std::array front{1.0f, 0.0f, 0.0f, 1.0f / 15.0f}; + auto before = Fill(block, 301u); + auto expected = before; + const auto inside = [](std::uint32_t x) { return x < ScissorWidth; }; + const auto survivor = [&](std::uint32_t x, std::uint32_t y) { return inside(x) && ((x ^ y) & 1u) == 0u; }; + const auto expect = [&](const auto& select, std::uint32_t word, std::uint32_t wordMask) { + for (std::uint32_t y = 0; y < Height; ++y) { + for (std::uint32_t x = 0; x < Width; ++x) { + if (!select(x, y)) continue; + auto& stored = expected[layout.Offset(x, y) / 2u]; + stored = static_cast((stored & ~wordMask) | (word & wordMask)); + } + } + }; + DrawCase(device, color, 0xfu, 0xffffu, Float32Export, Float32Program(nearer, Kill::BeforeExport), {PositionInputs, LateKill, TriangleAt(0.5f), depth.Address(), true}); + expect(survivor, Packed(nearer), 0xffffu); + ExpectWords(device, block, layout, expected, before, "a full write at z 0.5 that kills a checkerboard and writes depth"); + before = expected; + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(behind), {0u, 0u, TriangleAt(0.75f), depth.Address(), false}); + expect([&](std::uint32_t x, std::uint32_t y) { return inside(x) && !survivor(x, y); }, Packed(behind), 0xf000u); + ExpectWords(device, block, layout, expected, before, "an X merge at z 0.75 behind the depth 0.5 the surviving pixels hold"); + before = expected; + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(front), {0u, 0u, TriangleAt(0.25f), depth.Address(), false}); + expect([&](std::uint32_t x, std::uint32_t) { return inside(x); }, Packed(front), 0xf000u); + ExpectWords(device, block, layout, expected, before, "an X merge at z 0.25 in front of every stored depth"); } void ExpectKilled(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) { @@ -353,43 +457,48 @@ int main() { const auto color = DecodeTarget(block); Require(AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 1u) == 0xf000u && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 8u) == 0x000fu && AgcDriver::Graphics::PackedWriteMask(color.componentMapping, 5u) == 0xf0f0u, "the export components did not map to the guest nibbles (X in 15..12, W in 3..0)"); const std::array cases{{ - {0x1u, {1.0f, 0.0f, 0.0f, 0.0f}}, - {0x1u, {7.0f / 15.0f, 1.0f, 1.0f, 1.0f}}, - {0x5u, {0.2f, 0.4f, 0.6f, 0.8f}}, - {0x2u, {0.0f, 2.0f / 15.0f, 1.0f, 1.0f}}, - {0x8u, {1.0f, 1.0f, 1.0f, 11.0f / 15.0f}}, - {0x6u, {0.0f, 0.0f, 0.0f, 0.0f}}, - {0xeu, {0.0f, 1.0f, 0.5f, 0.25f}}, - {0xfu, {0.1f, 0.7f, 0.9f, 0.3f}}, - {0x1u, {0.0f, 0.0f, 0.0f, 0.0f}}, + {0x1u, 0xf000u, {1.0f, 0.0f, 0.0f, 0.0f}}, + {0x1u, 0xf000u, {7.0f / 15.0f, 1.0f, 1.0f, 1.0f}}, + {0x5u, 0xf0f0u, {0.2f, 0.4f, 0.6f, 0.8f}}, + {0x2u, 0x0f00u, {0.0f, 2.0f / 15.0f, 1.0f, 1.0f}}, + {0x8u, 0x000fu, {1.0f, 1.0f, 1.0f, 11.0f / 15.0f}}, + {0x6u, 0x0ff0u, {0.0f, 0.0f, 0.0f, 0.0f}}, + {0xeu, 0x0fffu, {0.0f, 1.0f, 0.5f, 0.25f}}, + {0xfu, 0xffffu, {0.1f, 0.7f, 0.9f, 0.3f}}, + {0x1u, 0xf000u, {0.0f, 0.0f, 0.0f, 0.0f}}, }}; + const std::array goldens{0xf000u, 0x7fffu, 0x369cu, 0x02ffu, 0xfffbu, 0x0000u, 0x0f84u, 0x2ad5u, 0x0000u}; auto before = Fill(block, 0); - for (const auto& [mask, rgba] : cases) { - const auto wordMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba)); - ExpectSurface(*device, block, layout, before, Packed(rgba), mask == 0xfu ? 0xffffu : wordMask, "a 32_ABGR " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); + for (std::size_t index = 0; index < cases.size(); ++index) { + const auto& [mask, wordMask, rgba] = cases[index]; + DrawCase(*device, color, mask, wordMask, Float32Export, Float32Program(rgba)); + ExpectSurface(*device, block, layout, before, goldens[index], wordMask, "a 32_ABGR " + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); before = ReadBack(*device, block); } const std::array b{2.0f / 15.0f, 1.0f, 1.0f, 1.0f}; const std::array c{0.6f, 0.2f, 0.8f, 0.4f}; const std::array d{1.0f, 0.0f, 9.0f / 15.0f, 0.0f}; - const std::array mergeThenFull{{{0x1u, b}, {0xfu, c}}}; - const std::array fullThenMerge{{{0xfu, c}, {0x1u, b}}}; - const std::array mergeThenMerge{{{0x1u, b}, {0x4u, d}, {0x8u, c}}}; + const std::array mergeThenFull{{{0x1u, 0xf000u, b}, {0xfu, 0xffffu, c}}}; + const std::array fullThenMerge{{{0xfu, 0xffffu, c}, {0x1u, 0xf000u, b}}}; + const std::array mergeThenMerge{{{0x1u, 0xf000u, b}, {0x4u, 0x00f0u, d}, {0x8u, 0x000fu, c}}}; BackToBack(*device, block, layout, color, mergeThenFull, 11, "a merged X write followed at once by a full write"); BackToBack(*device, block, layout, color, fullThenMerge, 12, "a full write followed at once by a merged X write"); BackToBack(*device, block, layout, color, mergeThenMerge, 13, "merged X, Z and W writes drawn back to back"); before = Fill(block, 77); const std::array halves{0.5f, 0.75f, 0.125f, 0.25f}; - const auto wordMask = Draw(*device, color, 0x9u, Float16Export, Float16Program(halves)); + const auto wordMask = DrawCase(*device, color, 0x9u, 0xf00fu, Float16Export, Float16Program(halves)); ExpectSurface(*device, block, layout, before, Packed(halves), wordMask, "an FP16_ABGR " + Describe(0x9u, halves) + " into a tiled 4_4_4_4 unorm target"); for (const auto kill : {Kill::BeforeExport, Kill::AfterExport}) { - for (const auto& [mask, rgba] : {Case{0x1u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { + for (const auto& [mask, wordMask, rgba] : {Case{0x1u, 0xf000u, {0.6f, 0.2f, 0.8f, 0.4f}}, Case{0x6u, 0x0ff0u, {1.0f, 2.0f / 15.0f, 13.0f / 15.0f, 0.0f}}}) { before = Fill(block, 91u + mask); - const auto killedMask = Draw(*device, color, mask, Float32Export, Float32Program(rgba, kill), true); + const auto killedMask = DrawCase(*device, color, mask, wordMask, Float32Export, Float32Program(rgba, kill), {PositionInputs, LateKill}); ExpectKilled(*device, block, layout, before, Packed(rgba), killedMask, std::string(kill == Kill::BeforeExport ? "a late-test checkerboard kill before " : "a late-test checkerboard kill after ") + Describe(mask, rgba) + " into a tiled 4_4_4_4 unorm target"); } } - std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences and 4 checkerboard-killing draws merged into a %ux%u tiled target, the killed pixels and those outside the scissor kept\n", cases.size() + 1u, Width, Height); + SameDrawOverdraw(*device, block, layout, color, 0.2f, 0.8f, 401u); + SameDrawOverdraw(*device, block, layout, color, 0.8f, 0.2f, 402u); + DepthRejection(*device, block, layout, color); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences, 4 checkerboard-killing draws, 2 same-draw overdraws of 16 layers and a depth-rejected merge into a %ux%u tiled target, the killed, rejected and unscissored pixels kept\n", cases.size() + 1u, Width, Height); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From f4adaff1fecd99b7b916a212a6cf89c649142111 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:52:29 +0200 Subject: [PATCH 050/318] docs(agc): list the limits of merged single-sample 4_4_4_4 partial writes Occlusion counts are taken at the forced early tests, the refused merge draws and why a late depth write was not used, and the one GPU the merge was measured on. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 1 + docs/dev/debt/shader.md | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ef2ea9da23..b5ee7296d8 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -161,6 +161,7 @@ - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. +- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp) (libSceAgcDriver) - the single-sample merge is measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 4353f3bd8e..7d891da817 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,6 +22,8 @@ - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) +- [4_4_4_4 partial writes, occlusion queries](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merging pixel shader always declares `EarlyFragmentTests`, so by the Vulkan rules sample counting happens before the shader runs and an occlusion query counts pixels a kill shader then discards. Not measured. +- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, or kills pixels under late tests while the draw writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and an operation other than keep). On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so moving the depth write after the shader does not work there. ### Functional From 7d4776a20b5cf1e35cb172c470d94dcbda0bca7c Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:51:33 +0200 Subject: [PATCH 051/318] fix(agc): read and write the merged 4_4_4_4 target through a Coherent image OpBeginInvocationInterlockEXT and OpEndInvocationInterlockEXT order the critical sections of overlapping fragment invocations but perform no availability or visibility operations (Vulkan memory model, fragment shader interlock), so without Coherent a merge could read a word that an earlier overlapping invocation stored but did not make visible. The image variable the merged target is bound through is now decorated Coherent. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 19 +++++++++++++++++++ .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 1 + 2 files changed, 20 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index be91393e26..b33e96effa 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -7776,6 +7776,24 @@ std::string interlockShape(std::span words) { return shape; } +bool coherentImageRead(std::span words) { + std::map sources; + std::set coherent; + std::uint32_t image = 0; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpDecorate && words[at + 2] == spv::DecorationCoherent) coherent.insert(words[at + 1]); + if (op == spv::OpLoad || op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) sources[words[at + 2]] = words[at + 3]; + if (op == spv::OpImageRead) image = words[at + 3]; + } + for (std::size_t step = 0; image != 0 && step < 8u; ++step) { + if (coherent.contains(image)) return true; + const auto source = sources.find(image); + image = source == sources.end() ? 0u : source->second; + } + return false; +} + std::string interlockExits(std::span words, std::size_t& kills, std::size_t& terminates) { struct Block { std::vector ops; @@ -8877,6 +8895,7 @@ void unorm4_4_4_4TargetTests() { } const auto what = std::string(registration ? "a registration" : "a specialized") + " 4_4_4_4 merge module"; Require(merged.registrationModule == registration && reads == 1u && writes == 1u && interlockShape(words) == "capability extension ordered begin end " && declaresEarlyFragmentTests(merging), what + " did not read and write the target once inside a pixel interlock after early tests: " + interlockShape(words)); + Require(coherentImageRead(words), what + " does not read the target through a Coherent image variable"); const auto bound = std::ranges::any_of(merged.bindings, [&](const ShaderRecompiler::DescriptorBinding& binding) { if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages || binding.kind != ShaderRecompiler::DescriptorKind::StorageImage) return false; for (std::size_t offset = 0; offset + 8u <= binding.guestDescriptor.size(); offset += 8u) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index a8068f0415..2fa410afac 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -547,6 +547,7 @@ void DefineDescriptors(SpirvEmitterState& state) { const auto& image = *selected; const auto name = "image_" + std::to_string(static_cast(binding.kind)); state.imageVariables.at(ImageBindingIndex(binding.kind)) = Define(ArrayType(ImageType(state, image)), name.c_str(), spv::StorageClassUniformConstant); + if (std::ranges::any_of(binding.resources, [&](std::uint32_t resource) { return state.program.Info().images.at(resource).targetMerge; })) state.module.AddAnnotation(spv::OpDecorate, state.imageVariables.at(ImageBindingIndex(binding.kind)), spv::DecorationCoherent); if (image.dimension == RdnaImageDimension::Dim1D || image.dimension == RdnaImageDimension::Dim1DArray) { state.module.EmitCapability(image.resourceClass == ImageResourceClass::Sampled ? spv::CapabilitySampled1D : spv::CapabilityImage1D); } From 74376b9172c92f0e6332053ad77637456f9d3004 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:52:05 +0200 Subject: [PATCH 052/318] fix(agc): refuse partial 4_4_4_4 writes under merged coverage A one-sample target under a multisampled rasterizer whose OVERRASTERIZATION_AMOUNT matches (radeonsi line and polygon smoothing) decodes with mergedCoverage and is drawn through mixed-samples coverage reduction onto its one-sample attachments. A merging draw leaves the 4_4_4_4 target out of the attachments, a combination that is not tested, so a partial write of such a draw now throws; the limit is listed in the technical debt file. Co-Authored-By: Claude Sonnet 5.5 --- .../libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 13 +++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 15 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c83ef04670..42b0f2b502 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1491,6 +1491,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { for (std::uint32_t i = 0; i < 3; ++i) { if ((requiredBlendMask & (1u << i)) != 0) result.blendConstants[i] = requiredBlendConstants[i]; } + Require(!result.mergedCoverage || std::ranges::none_of(result.colors, [](const ColorTarget& color) { return color.partialWriteMask != 0; }), "partial writes of a 4_4_4_4 unorm color target under merged coverage (a one-sample target under a multisampled rasterizer) are not implemented"); if (!result.colors.empty()) { result.color = result.colors.front(); result.blend = result.blends[result.colors.front().exportIndex]; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index b33e96effa..f02f17e037 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8864,6 +8864,19 @@ void unorm4_4_4_4TargetTests() { Require(partial.depth.has_value() == ((control & 3u) != 0u) && merge.mask == 0xf000u && AgcDriver::Graphics::DepthStencilWrites(partial) == writes && merge.depthStencilWrites == writes, "a partial 4_4_4_4 write under " + what + (writes ? " did not report" : " reported") + " depth or stencil writes"); } } + { + auto smoothed = queue; + smoothed.context[0x2f8] = 0x0020c002u; + smoothed.context[0x292] = 3; + smoothed.context[0x3b8] = 0x0dc00000u; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) smoothed.context[0x2fe + pixel * 4u] = 0xcc44; + smoothed.context[0x31d] = smoothed.context.at(0x31d) & ~0x3f000u; + smoothed.context[0x201] = 0x02130000u; + const auto merged = AgcDriver::Graphics::DecodeState(smoothed); + Require(merged.samples == 4 && merged.mergedCoverage && merged.color.samples == 1 && merged.color.partialWriteMask == 0u, "a full write of a one-sample 4_4_4_4 unorm target under OVERRASTERIZATION_AMOUNT 2 did not decode as merged coverage"); + smoothed.context[0x8e] = 1; + expectFailure([&] { AgcDriver::Graphics::DecodeState(smoothed); }, "under merged coverage"); + } { changed.context[0x8e] = 1; const auto partial = AgcDriver::Graphics::DecodeState(changed); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b5ee7296d8..ee73a1660c 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -161,6 +161,7 @@ - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. +- [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. - [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp) (libSceAgcDriver) - the single-sample merge is measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. From 1babc10d42eaded55eb38c4bd2d9bdf06801aec1 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:30:39 +0200 Subject: [PATCH 053/318] fix(agc): refuse merged 4_4_4_4 writes from shaders that write memory under late tests A partial 4_4_4_4 write merges inside a pixel shader that declares early fragment tests. When the guest runs the shader under late tests, pixels that the depth or stencil test rejects still execute it on the console, so their buffer and image stores, atomics and GDS operations happen there. Under forced early tests they would be dropped. Such a merge now throws, unless the guest asked for early tests itself. The merged target's own image write does not count. A recompile test checks two things for a merging shader that adds to a buffer counter: under late tests it throws, and under the guest's early tests it compiles with its one atomic add beside the merge. The same shader without a merge still compiles under late tests. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/tests/Graphics.cpp | 22 +++++++++++++++++++ .../src/SpirvMemory/SpirvModuleSetup.cpp | 12 ++++++++++ docs/dev/debt/shader.md | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index f02f17e037..ffb2dbcec4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8952,6 +8952,28 @@ void unorm4_4_4_4TargetTests() { auto exporting = merging; exporting.context.pixel->depthExportEnable = true; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(exporting)); }, "exports depth"); + alignas(16) static std::array counted{}; + static const std::array counter{static_cast(reinterpret_cast(counted.data())), static_cast(reinterpret_cast(counted.data()) >> 32u) & 0xffffu, 16u, 0x31016facu}; + constexpr std::array countingCode{0x7e000281u, 0xe0c80000u, 0x80000000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto counting = packedExportRequest(changed, partial, countingCode); + counting.context.userData = counter; + counting.context.pixel->targetMerge = merging.context.pixel->targetMerge; + counting.target = merging.target; + counting.target.registrationModules = false; + counting.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + static const std::array atomicCapabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityInt64}; + counting.target.supportedCapabilities = atomicCapabilities; + counting.context.pixel->earlyZ = false; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(counting)); }, "writes guest memory (stores, atomics or GDS) under late tests"); + counting.context.pixel->earlyZ = true; + const auto earlyWords = ShaderRecompiler::Recompile(counting).spirv.Copy(); + std::size_t atomics = 0; + for (std::size_t at = 5; at < earlyWords.size() && (earlyWords[at] >> 16u) != 0; at += earlyWords[at] >> 16u) atomics += static_cast(earlyWords[at] & 0xffffu) == spv::OpAtomicIAdd ? 1u : 0u; + Require(atomics == 1u && interlockShape(earlyWords) == "capability extension ordered begin end " && declaresEarlyFragmentTests(counting), "a 4_4_4_4 merge whose shader adds to a counter under early tests did not keep its one atomic add beside the merge"); + auto lateMerge = counting; + lateMerge.context.pixel->earlyZ = false; + lateMerge.context.pixel->targetMerge = {}; + static_cast(ShaderRecompiler::Recompile(lateMerge)); } changed = queue; changed.context[0x8e] = 0xau; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 1110470d17..7170bb8f77 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -208,6 +208,15 @@ std::uint32_t IndexedWindowSlots(const IrProgram& program) { return static_cast(reached); } +bool WritesGuestMemory(const IrProgram& program) { + const auto& info = program.Info(); + const auto writtenImage = [](const ImageResource& image) { return !image.targetMerge && (image.written || image.atomic || image.atomic64); }; + const auto writtenModes = [&](const std::vector& modes) { return std::ranges::any_of(modes, writtenImage); }; + const auto writtenBuffer = [](const BufferResource& buffer) { return buffer.written || buffer.atomic; }; + const auto gds = [](const IrDescriptorBinding& binding) { return binding.kind == DescriptorBindingKind::Gds; }; + return info.bdaWrites || std::ranges::any_of(info.buffers, writtenBuffer) || std::ranges::any_of(info.images, writtenImage) || std::ranges::any_of(info.runtimeImageModes, writtenModes) || std::ranges::any_of(program.Metadata().bindings.descriptors, gds); +} + } void DefineModule(SpirvEmitterState& state) { @@ -387,6 +396,9 @@ void DefineModule(SpirvEmitterState& state) { if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ && pixel.mergeDepthStencilWrites))) { throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); } + if (merge && !pixel.psEarlyZ && WritesGuestMemory(state.program)) { + throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that writes guest memory (stores, atomics or GDS) under late tests cannot use: the pixels the tests reject would still write it"); + } if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 7d891da817..3107557cc3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -23,7 +23,7 @@ - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) - [4_4_4_4 partial writes, occlusion queries](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merging pixel shader always declares `EarlyFragmentTests`, so by the Vulkan rules sample counting happens before the shader runs and an occlusion query counts pixels a kill shader then discards. Not measured. -- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, or kills pixels under late tests while the draw writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and an operation other than keep). On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so moving the depth write after the shader does not work there. +- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, or kills pixels under late tests while the draw writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and an operation other than keep). It also throws when its shader writes guest memory (buffer or image stores, atomics, GDS) under late tests, since the early tests would drop the writes of the pixels they reject. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so moving the depth write after the shader does not work there. ### Functional From 7ca6e1e9d213b4a145b35c0da53b37e0dba73fb9 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:30:50 +0200 Subject: [PATCH 054/318] fix(agc): refuse killing 4_4_4_4 merges while occlusion queries count samples A merged 4_4_4_4 write runs its pixel shader after early depth and stencil tests. Vulkan counts a sample for an occlusion query at those tests, so pixels the guest shader kills under late tests would be counted, and on the console they are not. Once the game has read an occlusion counter the recorder counts samples for every later draw, so from then on such a merge throws. The check runs where the recorded and recipe draw paths prepare their inputs. The state records DB_SHADER_CONTROL.KILL_ENABLE for it, which fits in the existing padding. The execution test starts sample counting and then checks two cases: a merge that kills a checkerboard under late tests is refused, and a merge that kills nothing still draws. A decode test reads KILL_ENABLE. Co-Authored-By: Claude Sonnet 5.5 --- .../src/Driver/Draw/DrawRegisterKey.cpp | 2 +- .../Graphics/include/Recorder.hpp | 1 + .../Graphics/include/State.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 4 +++ .../libSceAgcDriver/Graphics/src/State.cpp | 1 + .../prx/libSceAgcDriver/tests/Graphics.cpp | 6 +++++ .../execution/PackedUnorm4444PartialWrite.cpp | 26 ++++++++++++++++++- docs/dev/debt/shader.md | 3 +-- 8 files changed, 40 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index c0e563f02e..c2eb095b84 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -263,7 +263,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false; } if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.targetMerge != q.targetMerge || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false; - if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false; + if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ || s.pixelKill != t.pixelKill) return false; return true; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 169dcabeb6..78c3fc12ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -213,6 +213,7 @@ public: // Submits and waits for every batch, running completions in order. void Sync(); void CountSamples(); + bool CountingSamples() const { return countingSamples; } bool RecordMeshArguments(VkCommandBuffer commands, VkDeviceAddress record, VkDeviceAddress arguments, std::span rules); std::uint64_t SamplesTotal(); bool DumpSamples(VkDeviceAddress target, std::uint64_t address = 0); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 69802b363a..d7773aeba9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -155,6 +155,7 @@ struct State { std::uint32_t paClVsOutCntl = 0; std::array quadSampleMask{0xffffu, 0xffffu, 0xffffu, 0xffffu}; bool pixelEarlyZ = false; + bool pixelKill = false; }; // PA_CL_VS_OUT_CNTL bits that place position exports (clip and cull distances, the misc and the two diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 52ef639841..47f016563f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1093,6 +1093,10 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK"); Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target"); if (draw.indexed) RequireNoMultisampleTarget(draw.indexAddress, static_cast(indexBytes), "an index buffer read"); + if (state.pixelKill && !state.pixelEarlyZ && std::ranges::any_of(state.colors, [](const ColorTarget& color) { return color.partialWriteMask != 0; })) { + const auto* recorder = Recorder::Active(); + Require(recorder == nullptr || !recorder->CountingSamples(), "a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests cannot use while occlusion queries count samples: the early tests would count the pixels it kills"); + } APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders"); if (recipe != nullptr) { inputs.fragmentOutputs = recipe->fragmentOutputs; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 42b0f2b502..5593813041 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1352,6 +1352,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { } result.quadSampleMask = decodeQuadSampleMask(cx, result.samples); result.pixelEarlyZ = !PixelProgramSkipped(queue) && PixelEarlyZ(read(cx, 0x203)); + result.pixelKill = !PixelProgramSkipped(queue) && (read(cx, 0x203) & 0x40u) != 0; if ((read(cx, 0x207) & LayerExports & ~result.layerExports & ~(1u << 21u)) != 0) { static bool reported = false; if (!reported) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index ffb2dbcec4..82849cb1ee 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8880,6 +8880,12 @@ void unorm4_4_4_4TargetTests() { { changed.context[0x8e] = 1; const auto partial = AgcDriver::Graphics::DecodeState(changed); + auto killQueue = changed; + killQueue.context[0x203] = 0x40u; + const auto killState = AgcDriver::Graphics::DecodeState(killQueue); + killQueue.context[0x203] = 0x10u; + const auto earlyState = AgcDriver::Graphics::DecodeState(killQueue); + Require(killState.pixelKill && !killState.pixelEarlyZ && !earlyState.pixelKill && earlyState.pixelEarlyZ, "DB_SHADER_CONTROL.KILL_ENABLE of a 4_4_4_4 merge did not decode as a kill under late tests"); constexpr std::array mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u}; auto merging = packedExportRequest(changed, partial, mergeCode); merging.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(partial); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp index 394411849b..1acad9581f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "prx/libSceAgcDriver/Graphics/include/State.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -254,6 +256,7 @@ std::uint32_t Draw(AgcDriver::VulkanDevice& device, AgcDriver::Graphics::ColorTa state.blends = {blend}; state.blend = blend; state.blendConstants = {}; + state.pixelKill = (setup.shaderControl & LateKill) != 0u; if (setup.depth != 0) { state.depth = AgcDriver::Graphics::DepthTarget{setup.depth, 0, {Width, Height}, VK_FORMAT_D32_SFLOAT, 1.0f, 0, 0, false}; state.depthTest = true; @@ -421,6 +424,26 @@ void DepthRejection(AgcDriver::VulkanDevice& device, Block& block, const ColorTa ExpectWords(device, block, layout, expected, before, "an X merge at z 0.25 in front of every stored depth"); } +void CountedKill(AgcDriver::VulkanDevice& device, Block& block, const ColorTargetLayout& layout, const AgcDriver::Graphics::ColorTarget& color) { + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + auto* recorder = AgcDriver::Graphics::Recorder::Active(); + Require(recorder != nullptr, "the test device has no command recorder to count samples"); + recorder->CountSamples(); + } + const auto before = Fill(block, 501u); + const std::array rgba{0.6f, 0.2f, 0.8f, 0.4f}; + std::string refusal; + try { + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(rgba, Kill::BeforeExport), {PositionInputs, LateKill}); + } catch (const std::exception& error) { + refusal = error.what(); + } + Require(refusal.find("while occlusion queries count samples") != std::string::npos, "a merge that kills under late tests drew while occlusion queries count samples" + (refusal.empty() ? std::string() : ": " + refusal)); + DrawCase(device, color, 0x1u, 0xf000u, Float32Export, Float32Program(rgba)); + ExpectSurface(device, block, layout, before, Packed(rgba), 0xf000u, "a merged X write that kills nothing while occlusion queries count samples"); +} + void ExpectKilled(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector& before, std::uint32_t word, std::uint32_t wordMask, const std::string& what) { const auto stored = ReadBack(device, block); for (std::uint32_t y = 0; y < Height; ++y) { @@ -498,7 +521,8 @@ int main() { SameDrawOverdraw(*device, block, layout, color, 0.2f, 0.8f, 401u); SameDrawOverdraw(*device, block, layout, color, 0.8f, 0.2f, 402u); DepthRejection(*device, block, layout, color); - std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences, 4 checkerboard-killing draws, 2 same-draw overdraws of 16 layers and a depth-rejected merge into a %ux%u tiled target, the killed, rejected and unscissored pixels kept\n", cases.size() + 1u, Width, Height); + CountedKill(*device, block, layout, color); + std::printf("4_4_4_4 unorm partial write tests passed: %zu masked draws, 3 back-to-back sequences, 4 checkerboard-killing draws, 2 same-draw overdraws of 16 layers, a depth-rejected merge and a refused killing merge while samples are counted, into a %ux%u tiled target, the killed, rejected and unscissored pixels kept\n", cases.size() + 1u, Width, Height); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 3107557cc3..f8bfaff326 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,8 +22,7 @@ - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) -- [4_4_4_4 partial writes, occlusion queries](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merging pixel shader always declares `EarlyFragmentTests`, so by the Vulkan rules sample counting happens before the shader runs and an occlusion query counts pixels a kill shader then discards. Not measured. -- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, or kills pixels under late tests while the draw writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and an operation other than keep). It also throws when its shader writes guest memory (buffer or image stores, atomics, GDS) under late tests, since the early tests would drop the writes of the pixels they reject. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so moving the depth write after the shader does not work there. +- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, kills pixels under late tests in a draw that writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and a non-keep operation), writes guest memory (stores, atomics, GDS) under late tests, as the forced early tests would drop the writes of rejected pixels, or kills pixels under late tests after the game has read an occlusion counter, as the early tests would count them. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so the depth write cannot move after the shader there. ### Functional From 6ccb8c052e4c325cf761074f0f4590ce3b051fd4 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:31:05 +0200 Subject: [PATCH 055/318] fix(agc): refuse memory-writing 4_4_4_4 merges only where tests can reject pixels The refusal of a merging shader that writes guest memory under late tests fired in two cases where the early tests change nothing. The first was a draw with no depth, stencil or depth-bounds test. Early tests reject no pixel there, so every write the console makes still happens. The target merge now carries whether the draw has such a test, and the refusal applies only then. This adds one byte to the version 40 merge fields of the recompile request. The second was any GDS binding, including a shader that only reads GDS. The refusal now looks for GDS writes, atomics, appends and consumes. Tests: the depth and stencil decode checks the new flag, and the static shader key depends on it. A counter-adding shader under late tests compiles without a test and throws with one. A late-test merge that reads GDS compiles, and one that adds to GDS throws. Co-Authored-By: Claude Sonnet 5.5 --- .../Execution/src/Driver/Draw/DrawLookup.cpp | 1 + .../libSceAgcDriver/Graphics/include/State.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/State.cpp | 5 +++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 18 ++++++++++++++++-- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 10 +++++++--- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 2 ++ .../Optimization/ShaderStageInputInfo.hpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 16 +++++++++++++--- .../Translation/src/ShaderInputInfoBuilder.cpp | 1 + .../tests/RequestSerializerTests.cpp | 6 +++--- docs/dev/debt/shader.md | 2 +- 13 files changed, 53 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 84a83584ca..35fe9d6955 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -79,6 +79,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.rasterPrimitive)); mix(pixel.ditheredAlphaToCoverage); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index d7773aeba9..4a34835f95 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -173,6 +173,7 @@ std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask); bool DepthStencilWrites(const State& state); +bool DepthStencilTests(const State& state); ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state); ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state); bool PartialSampleMask(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5593813041..87348ec646 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1537,6 +1537,10 @@ bool DepthStencilWrites(const State& state) { return state.depthWrite || (state.stencilTest && (writes(state.stencilFront) || writes(state.stencilBack))); } +bool DepthStencilTests(const State& state) { + return state.depth.has_value() && (state.depthTest || state.stencilTest || state.depthBoundsTest); +} + ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { ShaderRecompiler::TargetMerge merge; for (const auto& color : state.colors) { @@ -1547,6 +1551,7 @@ ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { merge.samples = static_cast(color.samples); merge.descriptor = EncodeTextureResource(SurfaceForTarget(color)); merge.depthStencilWrites = DepthStencilWrites(state); + merge.depthStencilTests = DepthStencilTests(state); } return merge; } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 82849cb1ee..de1e86f42d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8847,13 +8847,15 @@ void unorm4_4_4_4TargetTests() { for (const auto offset : {0x013u, 0x015u}) depthQueue.context[offset] = 0x200; depthQueue.context[0x005] = 0x300; depthQueue.context[0x01e] = 0x1; + depthQueue.context[0x008] = std::bit_cast(0.25f); + depthQueue.context[0x009] = std::bit_cast(0.75f); struct DepthStencilCase { std::uint32_t control; std::uint32_t ops; std::uint32_t masks; bool writes; }; - for (const auto& [control, ops, masks, writes] : {DepthStencilCase{0x00700710u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700712u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700714u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700716u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0u, 0x01ffff01u, false}, DepthStencilCase{0x00700711u, 0x00050050u, 0x0100ff01u, false}, DepthStencilCase{0x00700713u, 0u, 0x01ffff01u, false}}) { + for (const auto& [control, ops, masks, writes] : {DepthStencilCase{0x00700710u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700712u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700714u, 0x00050050u, 0x01ffff01u, false}, DepthStencilCase{0x00700716u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0x00050050u, 0x01ffff01u, true}, DepthStencilCase{0x00700711u, 0u, 0x01ffff01u, false}, DepthStencilCase{0x00700711u, 0x00050050u, 0x0100ff01u, false}, DepthStencilCase{0x00700713u, 0u, 0x01ffff01u, false}, DepthStencilCase{0x00700718u, 0x00050050u, 0x01ffff01u, false}}) { depthQueue.context[0x200] = control; depthQueue.context[0x10b] = ops; depthQueue.context[0x10c] = depthQueue.context[0x10d] = masks; @@ -8861,7 +8863,7 @@ void unorm4_4_4_4TargetTests() { Require(AgcDriver::Graphics::DrawRejection(depthQueue, false).empty(), "the draw precheck refused a partial 4_4_4_4 write under " + what); const auto partial = AgcDriver::Graphics::DecodeState(depthQueue); const auto merge = AgcDriver::Graphics::ExportTargetMerge(partial); - Require(partial.depth.has_value() == ((control & 3u) != 0u) && merge.mask == 0xf000u && AgcDriver::Graphics::DepthStencilWrites(partial) == writes && merge.depthStencilWrites == writes, "a partial 4_4_4_4 write under " + what + (writes ? " did not report" : " reported") + " depth or stencil writes"); + Require(partial.depth.has_value() == ((control & 0xbu) != 0u) && partial.depthBoundsTest == ((control & 8u) != 0u) && merge.mask == 0xf000u && AgcDriver::Graphics::DepthStencilWrites(partial) == writes && merge.depthStencilWrites == writes && merge.depthStencilTests == ((control & 0xbu) != 0u), "a partial 4_4_4_4 write under " + what + (writes ? " did not report" : " reported") + " depth or stencil writes"); } } { @@ -8970,12 +8972,24 @@ void unorm4_4_4_4TargetTests() { static const std::array atomicCapabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityInt64}; counting.target.supportedCapabilities = atomicCapabilities; counting.context.pixel->earlyZ = false; + static_cast(ShaderRecompiler::Recompile(counting)); + counting.context.pixel->targetMerge.depthStencilTests = true; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(counting)); }, "writes guest memory (stores, atomics or GDS) under late tests"); counting.context.pixel->earlyZ = true; const auto earlyWords = ShaderRecompiler::Recompile(counting).spirv.Copy(); std::size_t atomics = 0; for (std::size_t at = 5; at < earlyWords.size() && (earlyWords[at] >> 16u) != 0; at += earlyWords[at] >> 16u) atomics += static_cast(earlyWords[at] & 0xffffu) == spv::OpAtomicIAdd ? 1u : 0u; Require(atomics == 1u && interlockShape(earlyWords) == "capability extension ordered begin end " && declaresEarlyFragmentTests(counting), "a 4_4_4_4 merge whose shader adds to a counter under early tests did not keep its one atomic add beside the merge"); + constexpr std::array gdsReadCode{0xbefc03ffu, 0x00000100u, 0x7e000280u, 0xd8da0000u, 0x04000000u, 0xbf8c0000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto gdsReading = packedExportRequest(changed, partial, gdsReadCode); + gdsReading.context.pixel->targetMerge = counting.context.pixel->targetMerge; + gdsReading.context.pixel->earlyZ = false; + gdsReading.target = counting.target; + static_cast(ShaderRecompiler::Recompile(gdsReading)); + constexpr std::array gdsAddCode{0xbefc03ffu, 0x00000100u, 0x7e000280u, 0xd8020000u, 0x00000000u, 0xbf8c0000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto gdsAdding = gdsReading; + gdsAdding.shader.code = gdsAddCode; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(gdsAdding)); }, "writes guest memory (stores, atomics or GDS) under late tests"); auto lateMerge = counting; lateMerge.context.pixel->earlyZ = false; lateMerge.context.pixel->targetMerge = {}; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 99da8fb457..5838cd62b6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4835,6 +4835,10 @@ void verifyPixelRequestSerialization() { depthWritingMerge.context.pixel->targetMerge.depthStencilWrites = true; RecompileCacheKey::Build(depthWritingMerge, replayKey); require(key != replayKey, "the merge draw's depth or stencil writes did not change the static shader identity"); + auto depthTestingMerge = request; + depthTestingMerge.context.pixel->targetMerge.depthStencilTests = true; + RecompileCacheKey::Build(depthTestingMerge, replayKey); + require(key != replayKey, "the merge draw's depth or stencil tests did not change the static shader identity"); auto withoutMerge = request; withoutMerge.context.pixel->targetMerge = {}; RecompileCacheKey::Build(withoutMerge, replayKey); @@ -5118,12 +5122,12 @@ void verifyVertexInfoVersion19() { require(allBitsOffset == flagOffset + 2u, "the front-face all-bits flag does not follow the dithered coverage flag"); require(blendOffset == allBitsOffset + 18u, "the reversed-order blend code is not serialized after the front-face all-bits flag, quad pixel mask and quad sample masks"); constexpr std::size_t iterationBytes = 35u; - constexpr std::size_t mergeBytes = 39u; + constexpr std::size_t mergeBytes = 40u; const auto mergeOffset = static_cast(blendOffset + 4u + iterationBytes); auto merging = pixel; - merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true}; + merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true, true}; const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); - require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 39 bytes the version 40 pixel info adds after the pixel iteration fields"); + require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 40 bytes the version 40 pixel info adds after the stencil operation value flag"); require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); const auto withoutMerge = [&](std::vector bytes) { bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast(mergeBytes)); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index fea7c67b40..f0ba68b06f 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -221,6 +221,7 @@ private: append(key, value.targetMerge.mask); append(key, value.targetMerge.samples); append(key, value.targetMerge.depthStencilWrites); + append(key, value.targetMerge.depthStencilTests); } static void append(std::vector& key, const ShaderVertexResourceDestination& value) { diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 389ef2f0f3..cbe4b7c150 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -360,6 +360,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteU8(info.targetMerge.samples); for (const std::uint32_t word : info.targetMerge.descriptor) writer.WriteU32(word); writer.WriteBool(info.targetMerge.depthStencilWrites); + writer.WriteBool(info.targetMerge.depthStencilTests); } ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { @@ -436,6 +437,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { info.targetMerge.samples = reader.ReadU8(); for (std::uint32_t& word : info.targetMerge.descriptor) word = reader.ReadU32(); info.targetMerge.depthStencilWrites = reader.ReadBool(); + info.targetMerge.depthStencilTests = reader.ReadBool(); } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 4e271d4660..4a6923cfb1 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -199,6 +199,7 @@ struct ShaderPixelInputInfo { std::uint16_t mergeMask = 0; std::uint8_t mergeSamples = 1; bool mergeDepthStencilWrites = false; + bool mergeDepthStencilTests = false; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 7c56960968..9d508c26f3 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -157,6 +157,7 @@ struct TargetMerge { std::uint8_t samples = 1; std::array descriptor{}; bool depthStencilWrites = false; + bool depthStencilTests = false; bool operator==(const TargetMerge&) const = default; }; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 7170bb8f77..0f95c839c2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -213,8 +213,18 @@ bool WritesGuestMemory(const IrProgram& program) { const auto writtenImage = [](const ImageResource& image) { return !image.targetMerge && (image.written || image.atomic || image.atomic64); }; const auto writtenModes = [&](const std::vector& modes) { return std::ranges::any_of(modes, writtenImage); }; const auto writtenBuffer = [](const BufferResource& buffer) { return buffer.written || buffer.atomic; }; - const auto gds = [](const IrDescriptorBinding& binding) { return binding.kind == DescriptorBindingKind::Gds; }; - return info.bdaWrites || std::ranges::any_of(info.buffers, writtenBuffer) || std::ranges::any_of(info.images, writtenImage) || std::ranges::any_of(info.runtimeImageModes, writtenModes) || std::ranges::any_of(program.Metadata().bindings.descriptors, gds); + if (info.bdaWrites || std::ranges::any_of(info.buffers, writtenBuffer) || std::ranges::any_of(info.images, writtenImage) || std::ranges::any_of(info.runtimeImageModes, writtenModes)) return true; + const auto& memory = program.Resources().memoryInfo; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + const auto access = SharedAccessOf(inst->Opcode()); + if (access == SharedAccess::None || access == SharedAccess::Read) continue; + const auto index = inst->Flags().index; + if (index >= memory.size()) FailEmit("a shared memory access has no memory metadata"); + if (memory[index].kind == ResourceKind::Gds) return true; + } + } + return false; } } @@ -396,7 +406,7 @@ void DefineModule(SpirvEmitterState& state) { if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ && pixel.mergeDepthStencilWrites))) { throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); } - if (merge && !pixel.psEarlyZ && WritesGuestMemory(state.program)) { + if (merge && !pixel.psEarlyZ && pixel.mergeDepthStencilTests && WritesGuestMemory(state.program)) { throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that writes guest memory (stores, atomics or GDS) under late tests cannot use: the pixels the tests reject would still write it"); } if ((pixel.psEarlyZ && !pixel.psAlphaToCoverage && !pixel.QuadMasked()) || merge) { diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 6161de1552..9a05e31dc5 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -160,6 +160,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.mergeMask = pixel.targetMerge.mask; pixelStorage.mergeSamples = pixel.targetMerge.samples; pixelStorage.mergeDepthStencilWrites = pixel.targetMerge.depthStencilWrites; + pixelStorage.mergeDepthStencilTests = pixel.targetMerge.depthStencilTests; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 00f6495be8..ef2304178d 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -54,7 +54,7 @@ void InsertMeshBarycentricFlag(std::vector& bytes) { void InsertPixelDefaultsAfterFrontFace(std::vector& bytes) { bytes.insert(bytes.begin() + 261u, {0x0fu, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0xffu, 0xffu, 0u, 0u, 0u, 0u, 0u, 0u}); - bytes.insert(bytes.begin() + 282u, 74u, 0u); + bytes.insert(bytes.begin() + 282u, 75u, 0u); bytes[322] = 1u; } @@ -411,7 +411,7 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38); auto upgraded = main38; - if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 356u); + if (fixture.pixel) main38.erase(main38.begin() + 317u, main38.begin() + 357u); upgraded[4] = 40u; if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); @@ -500,7 +500,7 @@ int main() { auto version35 = DecodeBase64(serializer.Serialize(request)); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); Require(stencilOperationValue < version35.end() && *stencilOperationValue == 0u); - version35.erase(stencilOperationValue, stencilOperationValue + 1 + 39); + version35.erase(stencilOperationValue, stencilOperationValue + 1 + 40); version35[4] = 35u; version35.pop_back(); const auto replay35 = serializer.Deserialize(EncodeBase64(version35)); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index f8bfaff326..a4683beb10 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -22,7 +22,7 @@ - [s_barrier in vertex-path programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (recompiler) - emits no instruction when the program has no LDS or GDS access and no memory write or atomic, so no wave of the subgroup can observe another, or when the barrier runs before every memory access of the program (it is on the entry's straight-line path with no load, store, atomic, image or LDS access ahead of it), so it orders nothing; the `GS_ALLOC_REQ` such a barrier orders on NGG passthrough programs is already dropped on the vertex path. The Vertex execution model has no workgroup barrier. Other programs with LDS, GDS or memory writes throw. Not measured on hardware - [Vertex clip and cull distances](../../../core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp) (libSceAgcDriver) - `PA_CL_VS_OUT_CNTL` enable bit `s` of `CLIP_DIST_ENA`/`CULL_DIST_ENA` is taken to select component `s` of the `pos1`/`pos2` exports, with the Vulkan array index counted by the enabled bits below it, and `VS_OUT_CCDIST0_VEC_ENA` as bit 22; this follows the recompiler's existing position-export mapping, not hardware or documentation. The guest clip distances share one `gl_ClipDistance` array with the all-zero clip position distance, which takes the index after them. A vertex program whose own code has no `pos1`..`pos3` export is compiled and prepared without these bits (`PositionExportLayout`), so registration and draws agree on its artifact; an export in a subroutine it calls is not seen by that scan - [32_AR color exports](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - with `SPI_SHADER_COL_FORMAT` 3 the red value is export channel 0 and the alpha is export channel 1, the GFX10 layout of Mesa's [`emit_ps_color_export`](https://gitlab.freedesktop.org/mesa/mesa/-/blob/3e32b0e1f132231b7a972c1b0b5b06c7e16cf8b9/src/amd/common/nir/ac_nir_lower_ps_late.c#L400-L411), ACO and radeonsi in Mesa 22.0 ([aco_instruction_selection.cpp](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/amd/compiler/aco_instruction_selection.cpp#L11087-L11096), [si_shader_llvm_ps.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/a82920af6efa4a7a66919885c6477fbd8fdc7b9d/src/gallium/drivers/radeonsi/si_shader_llvm_ps.c#L323-L333)) and AMD's [LLPC](https://github.com/GPUOpen-Drivers/llpc/blob/40cb8d95ad8d6f7f1652e3fd47d39667594cce08/lgc/lowering/FragmentColorExport.cpp#L157-L168); before GFX10 the alpha was channel 3. Channels 2 and 3, which none of these compilers enables, are ignored; green and blue are 0 and the alpha of an export without channel 1 is 1.0, the defaults of the other formats, which no source gives. Not measured on hardware, neither on gfx1013 nor on a PC RDNA 2 GPU; pinned in [Pixel32ArExport.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/Pixel32ArExport.cpp) -- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, kills pixels under late tests in a draw that writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and a non-keep operation), writes guest memory (stores, atomics, GDS) under late tests, as the forced early tests would drop the writes of rejected pixels, or kills pixels under late tests after the game has read an occlusion counter, as the early tests would count them. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so the depth write cannot move after the shader there. +- [4_4_4_4 partial writes, refused draws](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) (shader recompiler) - a merge throws when its shader exports depth, stencil or a sample mask, uses alpha-to-coverage or a quad sample mask, kills pixels under late tests in a draw that writes depth or stencil (a depth write, or a stencil test with a nonzero write mask and a non-keep operation), writes guest memory (stores, atomics, GDS writes) under late tests in a draw with a depth, stencil or depth-bounds test, as the forced early tests would drop the writes of rejected pixels, or kills pixels under late tests after the game has read an occlusion counter, as the early tests would count them. On an RTX 4070 SUPER (driver 610.88) a fragment killed after early tests still wrote depth, and `FragDepth` was ignored with `EarlyFragmentTests`, so the depth write cannot move after the shader there. ### Functional From c5b42dbef1bef853e2db3554d54dd0bac5d3979b Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:19:51 +0200 Subject: [PATCH 056/318] feat(agc): merge partial 4_4_4_4 writes into multisampled color targets A partial write of a multisampled 4_4_4_4 target now merges per sample: the draw shades per sample, and the pixel shader reads and writes its own sample (gl_SampleID) of the target through a multisampled storage image whose T# describes the target's surface. Draw makes the multisampled target storable before it takes the attachment view, so the image swap of the first storage access cannot leave the framebuffer on the retired image, and it accepts the target among the draw's multisampled sources only as that one merge binding. An execution test draws a full write into a 2-sample target, merges an X write of a triangle that cuts through pixels, and loads every sample: covered samples hold the merged word, uncovered ones the first draw's. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 + .../Graphics/include/State.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 12 +- .../Graphics/src/GuestTextureResource.cpp | 8 +- .../libSceAgcDriver/Graphics/src/State.cpp | 17 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 32 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 4 +- ...PackedUnorm4444MultisamplePartialWrite.cpp | 316 ++++++++++++++++++ .../Optimization/src/ShaderInfoCollector.cpp | 3 +- .../SpirvBackend/SpirvEmitterHelpers.hpp | 2 +- .../SpirvMemory/SpirvDescriptors.hpp | 2 +- .../src/SpirvMemory/SpirvDescriptors.cpp | 9 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 14 +- .../src/ShaderInputInfoBuilder.cpp | 2 +- 14 files changed, 401 insertions(+), 23 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e37e75bd3a..f4bf57be49 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1958,6 +1958,8 @@ target_include_directories(agc_driver_packed_unorm4444_color_target_tests PRIVAT target_include_directories(agc_driver_packed_unorm4444_partial_write_tests PRIVATE $) +target_include_directories(agc_driver_packed_unorm4444_multisample_partial_write_tests PRIVATE $) + target_include_directories(agc_driver_color1555_target_tests PRIVATE $) target_include_directories(agc_driver_color565_target_tests PRIVATE $) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp index 4a34835f95..091b96701e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/State.hpp @@ -172,6 +172,7 @@ VkExtent2D SampleLocationGrid(const State& state); std::array ExportMappings(const State& state); std::array ExportPackings(const State& state); std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t exportedMask); +GuestTextureResource TargetMergeSurface(const ColorTarget& color); bool DepthStencilWrites(const State& state); bool DepthStencilTests(const State& state); ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 47f016563f..360750c7dd 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2212,6 +2212,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& if (color.samples > 1) { binding.multisampled = true; timer.phase(PhaseSetup); + if (color.partialWriteMask != 0) { + static_cast(MultisampleTargetView(context, color)); + static_cast(MultisampleTargetStorageView(context, TargetMergeSurface(color))); + } targetViews.push_back(MultisampleTargetView(context, color, binding.multisampleOwner, &binding.multisampleImage)); timer.phase(PhaseReadTarget); continue; @@ -2438,10 +2442,14 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const bool multisampled = std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.multisampled; }); Require(lean || !multisampled, "multisampled draws are only rendered as recorded draws"); for (const auto& binding : targets) { - if (binding.multisampled) Require(std::find(resources->MultisampleSources().begin(), resources->MultisampleSources().end(), binding.color.surfaceAddress) == resources->MultisampleSources().end(), "sampling a multisampled color target the draw renders to is not implemented"); + if (!binding.multisampled) continue; + const auto& sources = resources->MultisampleSources(); + const auto reads = std::count(sources.begin(), sources.end(), binding.color.surfaceAddress); + Require(reads == 0 || (reads == 1 && binding.color.partialWriteMask != 0), "sampling a multisampled color target the draw renders to is not implemented"); + Require(binding.color.partialWriteMask == 0 || (reads == 1 && MultisampleTargetStorable(context, binding.color)), "the partial write of a multisampled packed color target does not store into the target's image"); } for (const auto& binding : targets) { - if (binding.color.partialWriteMask == 0) continue; + if (binding.color.partialWriteMask == 0 || binding.multisampled) continue; Require(lean && binding.resident != nullptr && !binding.proxied, "a partial write of a packed color target is merged into its resident image by a recorded draw only"); const auto images = resources->StorageImages(); Require(std::ranges::any_of(images, [&](const std::pair& image) { return image.first == binding.resident->Image() && image.second; }), "the partial write of a packed color target does not store into the target's resident image"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 1acb77e3d9..3fca810a43 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include +#include #include #include @@ -243,7 +244,8 @@ GuestTextureResource DecodeTextureResource(std::span words) } std::array EncodeTextureResource(const GuestTextureResource& resource) { - Require(resource.dimension == TextureDimension::k2D && resource.samples == 1u, "only single-sample 2D surfaces have a texture descriptor encoding"); + const bool multisampled = resource.dimension == TextureDimension::k2DMsaa; + Require((resource.dimension == TextureDimension::k2D && resource.samples == 1u) || (multisampled && resource.samples >= 2u && resource.samples <= 16u && std::has_single_bit(resource.samples)), "only 2D and multisampled 2D surfaces have a texture descriptor encoding"); Require(resource.mipCount == 1u && resource.allocatedMipCount <= 1u && resource.baseLevel == 0u && resource.lastLevel == 0u && resource.depthOrLastArray == 0u && resource.baseArray == 0u && resource.dccAddress == 0u && resource.minLod == 0u && !resource.iterate256 && resource.bcSwizzle == 0u, "only single-level, single-layer surfaces without metadata have a texture descriptor encoding"); Require(resource.width >= 1u && resource.width <= 0x4000u && resource.height >= 1u && resource.height <= 0x4000u && resource.format < 0x200u, "the surface's extent or format has no texture descriptor encoding"); const auto xorMask = resource.tileMode == TextureTileMode::kS4KBX || resource.tileMode == TextureTileMode::kD4KBX ? 0xfffull : 0xffffull; @@ -268,11 +270,13 @@ std::array EncodeTextureResource(const GuestTextureResource& r case TextureTileMode::kD64KBX: tileMode = 0x1a; break; case TextureTileMode::kR64KBX: tileMode = 0x1b; break; } + const auto sampleLevel = static_cast(std::countr_zero(resource.samples)); std::array words{}; words[0] = static_cast(base); words[1] = static_cast(base >> 32u) | (resource.format << 20u) | (((resource.width - 1u) & 3u) << 30u); words[2] = ((resource.width - 1u) >> 2u) | ((resource.height - 1u) << 14u); - words[3] = resource.dstSelX | (resource.dstSelY << 3u) | (resource.dstSelZ << 6u) | (resource.dstSelW << 9u) | (tileMode << 20u) | (9u << 28u); + words[3] = resource.dstSelX | (resource.dstSelY << 3u) | (resource.dstSelZ << 6u) | (resource.dstSelW << 9u) | (sampleLevel << 16u) | (tileMode << 20u) | ((multisampled ? 14u : 9u) << 28u); + words[5] = sampleLevel << 4u; return words; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 87348ec646..5fa91dcea9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1470,11 +1470,11 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { const std::uint32_t packedMask = nibbles ? 0xfu : 7u; Require(nibbles || (exportedMask & packedMask) == packedMask, "partial writes of " + packed + " color target are unsupported"); if ((exportedMask & packedMask) != packedMask) { - Require(color.samples == 1u, "partial writes of a multisampled " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented"); Require(color.mipCount == 1u && color.depth == 1u && color.layers == 1u && color.arrayLastSlice == 0u, "partial writes of a mipmapped, volume or array " + std::string(PackedColorTargetName(color.packing)) + " color target are not implemented"); Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "partial writes of more than one packed color target are not implemented"); color.partialWriteMask = PackedWriteMask(mapping, exportedMask); state.colorWriteMask = 0; + if (color.samples > 1u) result.perSampleShading = true; } } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { @@ -1531,6 +1531,16 @@ std::uint16_t PackedWriteMask(std::uint8_t componentMapping, std::uint32_t expor return mask; } +GuestTextureResource TargetMergeSurface(const ColorTarget& color) { + auto surface = SurfaceForTarget(color); + if (color.samples > 1u) { + Require(surface.baseAddress == color.surfaceAddress, "a merged multisampled color target does not start at its surface"); + surface.dimension = TextureDimension::k2DMsaa; + surface.samples = color.samples; + } + return surface; +} + bool DepthStencilWrites(const State& state) { if (!state.depth) return false; const auto writes = [](const VkStencilOpState& face) { return (face.writeMask & 0xffu) != 0 && (face.failOp != VK_STENCIL_OP_KEEP || face.passOp != VK_STENCIL_OP_KEEP || face.depthFailOp != VK_STENCIL_OP_KEEP); }; @@ -1545,11 +1555,12 @@ ShaderRecompiler::TargetMerge ExportTargetMerge(const State& state) { ShaderRecompiler::TargetMerge merge; for (const auto& color : state.colors) { if (color.partialWriteMask == 0) continue; - Require(merge.mask == 0 && color.exportIndex < 8u && color.samples == 1u, "a draw merges into one single-sample packed color target at most"); + Require(merge.mask == 0 && color.exportIndex < 8u && color.samples <= 8u, "a draw merges into one packed color target at most"); + Require(color.samples == 1u || state.perSampleShading, "a merged multisampled color target needs per-sample shading"); merge.target = static_cast(color.exportIndex); merge.mask = color.partialWriteMask; merge.samples = static_cast(color.samples); - merge.descriptor = EncodeTextureResource(SurfaceForTarget(color)); + merge.descriptor = EncodeTextureResource(TargetMergeSurface(color)); merge.depthStencilWrites = DepthStencilWrites(state); merge.depthStencilTests = DepthStencilTests(state); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index de1e86f42d..79d3ed27dd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -8996,9 +8996,37 @@ void unorm4_4_4_4TargetTests() { static_cast(ShaderRecompiler::Recompile(lateMerge)); } changed = queue; - changed.context[0x8e] = 0xau; + changed.context[0x8e] = 1u; changed.context[0x31d] = (changed.context.at(0x31d) & ~0x3f000u) | (1u << 12u) | (1u << 15u); - expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of a multisampled 4_4_4_4 unorm color target"); + { + const auto multisampled = AgcDriver::Graphics::DecodeState(changed); + Require(multisampled.samples == 2u && multisampled.color.samples == 2u && multisampled.color.partialWriteMask == 0xf000u && multisampled.blends[0].colorWriteMask == 0u && multisampled.perSampleShading, "a partial write of a 2-sample 4_4_4_4 unorm target did not merge per sample behind a closed write mask"); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(multisampled); + const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); + Require(merge.samples == 2u && merge.mask == 0xf000u && surface.dimension == AgcDriver::Graphics::TextureDimension::k2DMsaa && surface.samples == 2u && surface.baseAddress == multisampled.color.surfaceAddress && surface.width == multisampled.color.extent.width && surface.height == multisampled.color.extent.height && surface.format == AgcDriver::Graphics::TargetMergeSurface(multisampled.color).format, "the merge descriptor of a 2-sample 4_4_4_4 unorm target does not decode to its multisampled surface"); + auto unshaded = multisampled; + unshaded.perSampleShading = false; + expectFailure([&] { static_cast(AgcDriver::Graphics::ExportTargetMerge(unshaded)); }, "needs per-sample shading"); + constexpr std::array mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u}; + auto merging = packedExportRequest(changed, multisampled, mergeCode); + merging.context.pixel->targetMerge = merge; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityStorageImageMultisample}; + merging.target.supportedCapabilities = capabilities; + const auto words = ShaderRecompiler::Recompile(merging).spirv.Copy(); + std::size_t sampled = 0; + bool sampleRate = false; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + const auto op = static_cast(words[at] & 0xffffu); + if (op == spv::OpCapability && words[at + 1] == spv::CapabilitySampleRateShading) sampleRate = true; + if (op == spv::OpImageRead && (words[at] >> 16u) == 7u && words[at + 5] == spv::ImageOperandsSampleMask) ++sampled; + if (op == spv::OpImageWrite && (words[at] >> 16u) == 6u && words[at + 4] == spv::ImageOperandsSampleMask) ++sampled; + } + Require(sampleRate && sampled == 2u && interlockShape(words) == "capability extension ordered begin end ", "a 2-sample 4_4_4_4 merge module does not read and write the shaded sample inside a pixel interlock"); + auto single = merging; + const std::array noMultisampleStorage{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat}; + single.target.supportedCapabilities = noMultisampleStorage; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(single)); }, "storage image multisampling"); + } for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { changed = queue; changed.context[0x31c] = (changed.context[0x31c] & ~((7u << 8u) | (3u << 11u))) | (number << 8u) | (swap << 11u); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 5838cd62b6..1790340d6c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5125,9 +5125,9 @@ void verifyVertexInfoVersion19() { constexpr std::size_t mergeBytes = 40u; const auto mergeOffset = static_cast(blendOffset + 4u + iterationBytes); auto merging = pixel; - merging.context.pixel->targetMerge = {3u, 0xf00fu, 1u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true, true}; + merging.context.pixel->targetMerge = {3u, 0xf00fu, 2u, {1u, 2u, 3u, 4u, 5u, 6u, 7u, 0x89abcdefu}, true, true}; const auto mergingBytes = decodeBase64(serializer.Serialize(merging)); - require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 1u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 40 bytes the version 40 pixel info adds after the stencil operation value flag"); + require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 2u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast(mergeBytes)), "the color target merge is not the 40 bytes the version 40 pixel info adds after the stencil operation value flag"); require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge"); const auto withoutMerge = [&](std::vector bytes) { bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast(mergeBytes)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp new file mode 100644 index 0000000000..0a01c8cf18 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -0,0 +1,316 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "Optimization/ResourceProgram.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +struct VulkanDeviceTestAccess { + static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); } +}; + +} + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Side = 16; +constexpr std::uint32_t Samples = 2; +constexpr std::uint32_t SampleShift = 1; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); +constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); +constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; +constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Words = 32; +constexpr std::uint32_t LoadResult = 12; +constexpr double EdgeMargin = 1.0 / 32.0; +constexpr std::array First{0.2f, 0.4f, 0.6f, 0.8f}; +constexpr std::array Second{13.0f / 15.0f, 0.0f, 1.0f, 0.0f}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xbf8c3f70, 0xf0001f30, 0x00011005, + 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, + 0x80001301, 0xbf810000, +}; + +constexpr std::array, 3> Covering{{ + {-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, 3> Cutting{{ + {-0.93f, -1.0f, 0.5f, 1.0f}, {0.37f, -1.0f, 0.5f, 1.0f}, {-0.21f, 0.94f, 0.5f, 1.0f} +}}; + +alignas(256) std::array, 4> Programs{}; +std::size_t programCount = 0; +alignas(256) std::array Buffer{}; + +std::uint32_t Unorm(float value) { + if (!(value > 0.0f)) return 0; + if (value >= 1.0f) return 15; + return static_cast(std::nearbyint(static_cast(value) * 15.0)); +} + +std::uint32_t Packed(std::array rgba) { + return Unorm(rgba[3]) | (Unorm(rgba[2]) << 4u) | (Unorm(rgba[1]) << 8u) | (Unorm(rgba[0]) << 12u); +} + +std::span Float32Program(std::array rgba) { + Require(programCount < Programs.size(), "too many test programs"); + auto& code = Programs[programCount++]; + std::size_t at = 0; + for (std::uint32_t vgpr = 0; vgpr < 4u; ++vgpr) { + code[at++] = 0x7e0002ffu | ((4u + vgpr) << 17u); + code[at++] = std::bit_cast(rgba[vgpr]); + } + code[at++] = 0xf800180fu; + code[at++] = 0x07060504u; + code[at++] = 0xbf810000u; + return std::span(code.data(), at); +} + +struct Block { + explicit Block(std::size_t bytes) : bytes(bytes) { +#ifdef _WIN32 + data = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + data = static_cast(std::aligned_alloc(BlockBytes, bytes)); +#endif + Require(data != nullptr, "cannot allocate the multisampled color target"); + std::memset(data, 0, bytes); + GuestAllocations::Mutation mutation; + mutation.Add(data, bytes, true, true, true); + } + ~Block() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(data); + } +#ifdef _WIN32 + VirtualFree(data, 0, MEM_RELEASE); +#else + std::free(data); +#endif + } + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + std::uint64_t Address() const { return reinterpret_cast(data); } + std::size_t bytes; + std::uint8_t* data = nullptr; +}; + +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Unorm4_4_4_4Info; + cx[0x31d] = MultisampledAttrib; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 packed R16_UINT surface of 2 samples"); + return color; +} + +AgcDriver::Graphics::State TargetState(AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask) { + const bool partial = exportedMask != 0xfu; + color.partialWriteMask = partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u; + VkPipelineColorBlendAttachmentState blend{}; + blend.colorWriteMask = partial ? 0u : 0xfu; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.colors = {color}; + state.color = color; + state.hasColorTarget = true; + state.renderExtent = {Side, Side}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Side), static_cast(Side), -static_cast(Side), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Side, Side}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blends = {blend}; + state.blend = blend; + state.blendConstants = {}; + state.samples = Samples; + state.perSampleShading = partial; + return state; +} + +void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& state, const std::array, 3>& triangle, std::span pixelCode) { + const auto target = device.Target(); + constexpr std::uint32_t waveSize = 64; + std::vector vertexUserData(4, 0u); + const auto address = reinterpret_cast(triangle.data()); + const std::array vertexBuffer{static_cast(address), static_cast((address >> 32u) & 0xffffu) | (16u << 16u), static_cast(triangle.size()), 0x01016facu}; + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + const auto codeAddress = reinterpret_cast(pixelCode.data()); + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, + {waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + const auto handle = ShaderRecompiler::PrepareShader(fragment); + AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}}; + snapshot.prepared->entries.push_back({0, handle}); + fragment.shader.code = snapshot.code; + fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state)); + fragment.context.pixel->targetExportPacking = AgcDriver::Graphics::ExportPackings(state); + fragment.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(state); + const ShaderRecompiler::SrtRuntime runtime{}; + const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, fragment); + const auto pixelResult = invocation.Materialize(*invocation.Capture(runtime)); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)} + }}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(triangle.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); +} + +std::vector LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture) { + std::vector userData(16, 0u); + const auto address = reinterpret_cast(Buffer.data()); + const std::array buffer{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; + std::copy(buffer.begin(), buffer.end(), userData.begin()); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(LoadCode.data()), std::as_bytes(std::span(LoadCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(LoadCode.data()), LoadCode, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto program = ShaderRecompiler::Recompile(request); + std::vector loaded(Side * Side * Samples); + for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto index = first + tid; + const auto pixel = index / Samples; + const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) loaded[first + tid] = Buffer[tid * Words + LoadResult]; + } + return loaded; +} + +double EdgeDistance(double x, double y, std::size_t from) { + const auto pixels = [](const std::array& vertex) { + return std::array{(static_cast(vertex[0]) + 1.0) * 0.5 * Side, Side - (static_cast(vertex[1]) + 1.0) * 0.5 * Side}; + }; + const auto a = pixels(Cutting[from]); + const auto b = pixels(Cutting[(from + 1u) % Cutting.size()]); + const auto c = pixels(Cutting[(from + 2u) % Cutting.size()]); + const auto length = std::hypot(b[0] - a[0], b[1] - a[1]); + const auto side = [&](double px, double py) { return ((b[0] - a[0]) * (py - a[1]) - (b[1] - a[1]) * (px - a[0])) / length; }; + return side(c[0], c[1]) < 0 ? -side(x, y) : side(x, y); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); + if (!context.storageImageMultisample || !context.sampleRateShading || !TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT)) { + std::puts("skipped, the device lacks storage image multisampling, sample rate shading or fragmentShaderPixelInterlock"); + return VulkanTestSkipped; + } + Block block(4u * BlockBytes); + const auto color = DecodeTarget(block); + const auto full = TargetState(color, 0xfu); + Draw(*device, full, Covering, Float32Program(First)); + const auto merged = TargetState(color, 0x1u); + Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); + Draw(*device, merged, Cutting, Float32Program(Second)); + const auto texture = AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)); + const auto loaded = LoadSamples(*device, texture); + const auto background = Packed(First); + const auto written = (background & 0x0fffu) | (Packed(Second) & 0xf000u); + Require(written != background, "the test colors do not differ in the merged nibble"); + const auto locations = AgcDriver::Graphics::SampleLocations(merged); + std::array pixelsByCoverage{}; + std::uint32_t checked = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + std::uint32_t covered = 0; + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto word = loaded[(y * Side + x) * Samples + sample]; + char where[160]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) holds 0x%04x", sample, x, y, word); + Require(word == written || word == background, std::string(where) + ", neither the merged word 0x" + std::to_string(written) + " nor the first draw's word"); + covered += word == written ? 1u : 0u; + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto distance = std::min({EdgeDistance(px, py, 0), EdgeDistance(px, py, 1), EdgeDistance(px, py, 2)}); + if (std::fabs(distance) < EdgeMargin) continue; + ++checked; + Require((word == written) == (distance > 0), std::string(where) + (distance > 0 ? ", which the merging triangle covers" : ", which the merging triangle does not cover")); + } + ++pixelsByCoverage[covered]; + } + } + Require(checked >= Side * Side * Samples * 9u / 10u, "fewer than nine in ten samples are clear of the triangle's edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0 && pixelsByCoverage[1] != 0, "the merging triangle does not leave pixels uncovered, covered and partly covered"); + std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly covered pixels merged bits 15..12 into their covered samples only\n", pixelsByCoverage[1]); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index b56aea041c..d22d470487 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -528,7 +528,7 @@ void AddTargetMerge(const ShaderPixelInputInfo& pixel, ShaderInfo& info) { image.source = std::numeric_limits::max(); image.resourceClass = ImageResourceClass::Storage; image.numericClass = IrTextureNumericClass::Uint; - image.dimension = RdnaImageDimension::Dim2D; + image.dimension = pixel.mergeSamples > 1u ? RdnaImageDimension::Dim2DMsaa : RdnaImageDimension::Dim2D; image.read = true; image.written = true; image.fmaskCompatible = false; @@ -536,6 +536,7 @@ void AddTargetMerge(const ShaderPixelInputInfo& pixel, ShaderInfo& info) { info.images.push_back(image); info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(image)); AddInput(info, StageInputKind::FragCoord, 0, 4, "gl_FragCoord"); + if (pixel.mergeSamples > 1u) AddInput(info, StageInputKind::SampleId, 0, 1, "gl_SampleID"); } void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 4dde3cba3e..6c1539608c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -70,7 +70,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); bool TablePointSampled(const ImageResource& image); bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); -std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord); +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord, std::uint32_t sample); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); std::uint32_t ExecutionModelForStage(IrShaderStage stage); std::uint32_t ConstantI32(SpirvEmitterState& state, std::int32_t value); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp index 766da39c29..5ff7152418 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvDescriptors.hpp @@ -21,7 +21,7 @@ void DecorateTableNonUniform(SpirvEmitterState& state, std::uint32_t id); bool TablePointSampled(const ImageResource& image); bool TableModeEmitted(const ImageResource& mode); std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint32_t resource); -std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord); +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord, std::uint32_t sample); void EmitStorageImageWrite(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t mipLod, std::uint32_t coord, std::uint32_t texel, std::uint32_t sample); const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension); [[noreturn]] void ExitDescriptorBindingFailure(const SpirvEmitterState& state, DescriptorBindingKind kind, std::uint32_t resource, const char* reason); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index c9a3717039..7a5fe34450 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -233,7 +233,7 @@ std::uint32_t StorageImageDescriptorPointer(SpirvEmitterState& state, std::uint3 return DescriptorElementPointer(state, pointerType, variable, arrayIndex, kind, resource, "storage image descriptor array was not emitted"); } -std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord) { +std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resource, std::uint32_t coord, std::uint32_t sample) { const auto& image = state.program.Info().images.at(resource); if (image.resourceClass != ImageResourceClass::Storage || image.table != NoTable || image.mipMode == ImageMipMode::DynamicStorage || image.atomic) { FailEmit("storage image read requested for an image that is not one plain storage descriptor"); @@ -245,7 +245,12 @@ std::uint32_t EmitStorageImageRead(SpirvEmitterState& state, std::uint32_t resou state.module.AddFunction(spv::OpLoad, imageType, descriptor, StorageImageDescriptorPointer(state, resource)); const auto texel = state.module.AllocateId(); const auto texelType = ImageVectorType(state, image.numericClass, 4u); - state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord); + if (RdnaImageDimensionInfoFor(image.dimension).multisampled != 0u) { + if (sample == 0u) FailEmit("multisampled storage image read requires a sample"); + state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord, spv::ImageOperandsSampleMask, sample); + } else { + state.module.AddFunction(spv::OpImageRead, texelType, texel, descriptor, coord); + } return texel; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index c88b3f34ad..a070862e06 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -417,27 +417,29 @@ std::uint32_t SelectExportPacking(SpirvEmitterState& state, std::uint32_t target struct MergeTexel { std::uint32_t resource; std::uint32_t coord; + std::uint32_t sample; }; MergeTexel MergeTargetTexel(SpirvEmitterState& state) { + const auto& pixel = PixelInfo(state); const auto& images = state.program.Info().images; const auto image = std::ranges::find_if(images, [](const ImageResource& candidate) { return candidate.targetMerge; }); if (image == images.end()) FailEmit("the color target merge image was not collected"); - if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u) FailEmit("the color target merge inputs were not declared"); + if (InputVariableForKind(state, StageInputKind::FragCoord) == 0u || (pixel.mergeSamples > 1u && InputVariableForKind(state, StageInputKind::SampleId) == 0u)) FailEmit("the color target merge inputs were not declared"); if (state.mergeWordVariable == 0u || state.mergePendingVariable == 0u) FailEmit("the color target merge variables were not declared"); const auto position = [&](std::uint32_t component) { return Unary(state, spv::OpConvertFToU, TypeU32(state), Unary(state, spv::OpBitcast, TypeF32(state), EmitBuiltinU32(state, StageInputKind::FragCoord, component))); }; const auto x = position(0u); const auto y = position(1u); const auto coord = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 2u), coord, x, y); - return {static_cast(image - images.begin()), coord}; + return {static_cast(image - images.begin()), coord, pixel.mergeSamples > 1u ? EmitBuiltinU32(state, StageInputKind::SampleId, 0u) : 0u}; } void MergeColorTarget(SpirvEmitterState& state, std::uint32_t color) { const auto& pixel = PixelInfo(state); - const auto [resource, coord] = MergeTargetTexel(state); + const auto [resource, coord, sample] = MergeTargetTexel(state); const auto stored = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord), 0u); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stored, EmitStorageImageRead(state, resource, coord, sample), 0u); const auto word = PackExport(state, IrBufferFormat::Format4_4_4_4UNorm, color); const auto kept = EmitBinaryU32(state, spv::OpBitwiseAnd, stored, ConstantU32(state, ~static_cast(pixel.mergeMask))); const auto merged = EmitOrU32(state, kept, EmitBinaryU32(state, spv::OpBitwiseAnd, word, ConstantU32(state, pixel.mergeMask))); @@ -759,12 +761,12 @@ void FlushColorTargetMerge(SpirvEmitterState& state) { write = Binary(state, spv::OpLogicalAnd, TypeBool(state), write, Binary(state, spv::OpINotEqual, TypeBool(state), valid, ConstantU32(state, 0u))); } EmitIfCondition(state, write, [&] { - const auto [resource, coord] = MergeTargetTexel(state); + const auto [resource, coord, sample] = MergeTargetTexel(state); const auto merged = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), merged, state.mergeWordVariable); const auto texel = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4u), texel, merged, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)); - EmitStorageImageWrite(state, resource, 0u, coord, texel, 0u); + EmitStorageImageWrite(state, resource, 0u, coord, texel, sample); }); } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 9a05e31dc5..57c126d55a 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -155,7 +155,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psIteratesSamples = pixel.iteratesSamples; pixelStorage.psPositionLocation = pixel.positionLocation; pixelStorage.sampleLocations = pixel.sampleLocations; - if (pixel.targetMerge.mask != 0u && (pixel.targetMerge.target >= 8u || pixel.targetMerge.samples != 1u)) throw std::runtime_error("ShaderInputInfoBuilder: invalid color target merge"); + if (pixel.targetMerge.mask != 0u && (pixel.targetMerge.target >= 8u || (pixel.targetMerge.samples != 1u && pixel.targetMerge.samples != 2u && pixel.targetMerge.samples != 4u && pixel.targetMerge.samples != 8u))) throw std::runtime_error("ShaderInputInfoBuilder: invalid color target merge"); pixelStorage.mergeTarget = pixel.targetMerge.target; pixelStorage.mergeMask = pixel.targetMerge.mask; pixelStorage.mergeSamples = pixel.targetMerge.samples; From f0ea4fa5636bac3b59ab4868b6f2f5244c40ebba Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:20:07 +0200 Subject: [PATCH 057/318] fix(agc): settle merged multisampled writes before the draws that follow A multisampled target's storage writes are ordered for later draws only through MultisampleTarget::Storage(), which a reused resource build would skip. The merging draw now takes the storage view once more after its resources are resolved, so it is the last access before recording; multisampled draws make no draw recipe. The multisampled test draws a full write right after the merge and checks that it covers the merged samples it overlaps. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 1 + ...PackedUnorm4444MultisamplePartialWrite.cpp | 42 +++++++++++++------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 360750c7dd..673d8a3968 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2447,6 +2447,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters& const auto reads = std::count(sources.begin(), sources.end(), binding.color.surfaceAddress); Require(reads == 0 || (reads == 1 && binding.color.partialWriteMask != 0), "sampling a multisampled color target the draw renders to is not implemented"); Require(binding.color.partialWriteMask == 0 || (reads == 1 && MultisampleTargetStorable(context, binding.color)), "the partial write of a multisampled packed color target does not store into the target's image"); + if (binding.color.partialWriteMask != 0) static_cast(MultisampleTargetStorageView(context, TargetMergeSurface(binding.color))); } for (const auto& binding : targets) { if (binding.color.partialWriteMask == 0 || binding.multisampled) continue; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp index 0a01c8cf18..0e7f6d3031 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -55,6 +55,7 @@ constexpr std::uint32_t LoadResult = 12; constexpr double EdgeMargin = 1.0 / 32.0; constexpr std::array First{0.2f, 0.4f, 0.6f, 0.8f}; constexpr std::array Second{13.0f / 15.0f, 0.0f, 1.0f, 0.0f}; +constexpr std::array Third{1.0f / 15.0f, 1.0f, 0.0f, 4.0f / 15.0f}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -75,6 +76,10 @@ constexpr std::array, 3> Cutting{{ {-0.93f, -1.0f, 0.5f, 1.0f}, {0.37f, -1.0f, 0.5f, 1.0f}, {-0.21f, 0.94f, 0.5f, 1.0f} }}; +constexpr std::array, 3> Overlapping{{ + {-0.08f, -0.47f, 0.5f, 1.0f}, {1.0f, -0.13f, 0.5f, 1.0f}, {0.29f, 1.0f, 0.5f, 1.0f} +}}; + alignas(256) std::array, 4> Programs{}; std::size_t programCount = 0; alignas(256) std::array Buffer{}; @@ -248,18 +253,22 @@ std::vector LoadSamples(AgcDriver::VulkanDevice& device, const st return loaded; } -double EdgeDistance(double x, double y, std::size_t from) { +double EdgeDistance(const std::array, 3>& triangle, double x, double y, std::size_t from) { const auto pixels = [](const std::array& vertex) { return std::array{(static_cast(vertex[0]) + 1.0) * 0.5 * Side, Side - (static_cast(vertex[1]) + 1.0) * 0.5 * Side}; }; - const auto a = pixels(Cutting[from]); - const auto b = pixels(Cutting[(from + 1u) % Cutting.size()]); - const auto c = pixels(Cutting[(from + 2u) % Cutting.size()]); + const auto a = pixels(triangle[from]); + const auto b = pixels(triangle[(from + 1u) % triangle.size()]); + const auto c = pixels(triangle[(from + 2u) % triangle.size()]); const auto length = std::hypot(b[0] - a[0], b[1] - a[1]); const auto side = [&](double px, double py) { return ((b[0] - a[0]) * (py - a[1]) - (b[1] - a[1]) * (px - a[0])) / length; }; return side(c[0], c[1]) < 0 ? -side(x, y) : side(x, y); } +double Inside(const std::array, 3>& triangle, double x, double y) { + return std::min({EdgeDistance(triangle, x, y, 0), EdgeDistance(triangle, x, y, 1), EdgeDistance(triangle, x, y, 2)}); +} + } int main() { @@ -278,14 +287,18 @@ int main() { const auto merged = TargetState(color, 0x1u); Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); Draw(*device, merged, Cutting, Float32Program(Second)); + Draw(*device, full, Overlapping, Float32Program(Third)); const auto texture = AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)); const auto loaded = LoadSamples(*device, texture); const auto background = Packed(First); const auto written = (background & 0x0fffu) | (Packed(Second) & 0xf000u); - Require(written != background, "the test colors do not differ in the merged nibble"); + const auto over = Packed(Third); + Require(written != background && over != written && over != background, "the test colors do not differ in the merged nibble"); const auto locations = AgcDriver::Graphics::SampleLocations(merged); std::array pixelsByCoverage{}; std::uint32_t checked = 0; + std::uint32_t overdrawn = 0; + std::uint32_t mergedAfterOverdraw = 0; for (std::uint32_t y = 0; y < Side; ++y) { for (std::uint32_t x = 0; x < Side; ++x) { std::uint32_t covered = 0; @@ -293,21 +306,26 @@ int main() { const auto word = loaded[(y * Side + x) * Samples + sample]; char where[160]; std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) holds 0x%04x", sample, x, y, word); - Require(word == written || word == background, std::string(where) + ", neither the merged word 0x" + std::to_string(written) + " nor the first draw's word"); + Require(word == written || word == background || word == over, std::string(where) + ", neither the merged word, the first draw's word nor the last draw's word"); covered += word == written ? 1u : 0u; const auto px = x + static_cast(locations[sample].x); const auto py = y + static_cast(locations[sample].y); - const auto distance = std::min({EdgeDistance(px, py, 0), EdgeDistance(px, py, 1), EdgeDistance(px, py, 2)}); - if (std::fabs(distance) < EdgeMargin) continue; + const auto distance = Inside(Cutting, px, py); + const auto overDistance = Inside(Overlapping, px, py); + if (std::fabs(distance) < EdgeMargin || std::fabs(overDistance) < EdgeMargin) continue; ++checked; - Require((word == written) == (distance > 0), std::string(where) + (distance > 0 ? ", which the merging triangle covers" : ", which the merging triangle does not cover")); + const auto expected = overDistance > 0 ? over : distance > 0 ? written : background; + overdrawn += overDistance > 0 && distance > 0 ? 1u : 0u; + mergedAfterOverdraw += overDistance <= 0 && distance > 0 ? 1u : 0u; + Require(word == expected, std::string(where) + (overDistance > 0 ? ", which the last full write covers" : distance > 0 ? ", which only the merging triangle covers" : ", which no later triangle covers")); } ++pixelsByCoverage[covered]; } } - Require(checked >= Side * Side * Samples * 9u / 10u, "fewer than nine in ten samples are clear of the triangle's edges"); - Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0 && pixelsByCoverage[1] != 0, "the merging triangle does not leave pixels uncovered, covered and partly covered"); - std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly covered pixels merged bits 15..12 into their covered samples only\n", pixelsByCoverage[1]); + Require(checked >= Side * Side * Samples * 8u / 10u, "fewer than eight in ten samples are clear of the triangles' edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0 && pixelsByCoverage[1] != 0, "the merged samples do not leave pixels unmerged, merged and partly merged"); + Require(overdrawn != 0 && mergedAfterOverdraw != 0, "the last full write does not cover some merged samples and leave others"); + std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, and a full write drawn right after covered %u merged samples\n", pixelsByCoverage[1], overdrawn); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 2e8ea10af4216cc22e843d49b6ce648c3b1e047c Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:56:15 +0200 Subject: [PATCH 058/318] test(agc): decode the 2-sample 4_4_4_4 merge draws from registers The 2-sample test decodes its draw states from registers and checks the word mask, the closed write mask, per-sample shading and the exported merge, and a skip names the missing device feature. Co-Authored-By: Claude Sonnet 5.5 --- ...PackedUnorm4444MultisamplePartialWrite.cpp | 61 ++++++++++++++----- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp index 0e7f6d3031..b5fd107ed5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -152,15 +152,47 @@ AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { return color; } -AgcDriver::Graphics::State TargetState(AgcDriver::Graphics::ColorTarget color, std::uint32_t exportedMask) { +AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask) { + AgcDriver::QueueState queue; + queue.userConfig[0x242] = 4; + queue.context = { + {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, targetMask}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, Float32Export}, {0x1c3, 4}, + {0x318, static_cast(block.Address() >> 8u)}, {0x390, static_cast(block.Address() >> 40u)}, + {0x31b, 0}, {0x31c, Unorm4_4_4_4Info}, {0x31d, MultisampledAttrib}, + {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}, {0x1e0, 0}, + {0xc, 0}, {0xd, (Side << 16u) | Side}, + {0x81, 0x80000000}, {0x82, (Side << 16u) | Side}, + {0x90, 0x80000000}, {0x91, (Side << 16u) | Side}, + {0x94, 0x80000000}, {0x95, (Side << 16u) | Side}, + {0x10f, std::bit_cast(static_cast(Side) / 2.0f)}, {0x110, std::bit_cast(static_cast(Side) / 2.0f)}, + {0x111, std::bit_cast(-static_cast(Side) / 2.0f)}, {0x112, std::bit_cast(static_cast(Side) / 2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)} + }; + queue.shader[0x008] = 0x100; + queue.shader[0x009] = 0; + return queue; +} + +AgcDriver::Graphics::State TargetState(const Block& block, std::uint32_t exportedMask) { const bool partial = exportedMask != 0xfu; - color.partialWriteMask = partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u; - VkPipelineColorBlendAttachmentState blend{}; - blend.colorWriteMask = partial ? 0u : 0xfu; + const auto decoded = AgcDriver::Graphics::DecodeState(Queue(block, exportedMask)); + const auto& color = decoded.colors.front(); + char detail[256]; + std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x of the 2-sample 4_4_4_4 target decoded %zu targets, %u samples, word mask 0x%04x, write mask 0x%x and %s shading", exportedMask, decoded.colors.size(), decoded.samples, color.partialWriteMask, decoded.blends.front().colorWriteMask, decoded.perSampleShading ? "per-sample" : "pixel-rate"); + Require(decoded.colors.size() == 1u && decoded.samples == Samples && color.samples == Samples && color.partialWriteMask == (partial ? 0xf000u : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); + const auto merge = AgcDriver::Graphics::ExportTargetMerge(decoded); + Require(merge.mask == color.partialWriteMask && merge.samples == (partial ? Samples : 1u) && !merge.depthStencilWrites && merge.descriptor == (partial ? AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)) : std::array{}), "the decoded 2-sample target exported another merge than its own surface"); AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; - state.colors = {color}; - state.color = color; + state.colors = decoded.colors; + state.color = decoded.color; state.hasColorTarget = true; state.renderExtent = {Side, Side}; state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; @@ -169,11 +201,11 @@ AgcDriver::Graphics::State TargetState(AgcDriver::Graphics::ColorTarget color, s state.scissor = {{0, 0}, {Side, Side}}; state.cullMode = VK_CULL_MODE_NONE; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; - state.blends = {blend}; - state.blend = blend; + state.blends = decoded.blends; + state.blend = decoded.blend; state.blendConstants = {}; - state.samples = Samples; - state.perSampleShading = partial; + state.samples = decoded.samples; + state.perSampleShading = decoded.perSampleShading; return state; } @@ -276,15 +308,16 @@ int main() { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device); - if (!context.storageImageMultisample || !context.sampleRateShading || !TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT)) { - std::puts("skipped, the device lacks storage image multisampling, sample rate shading or fragmentShaderPixelInterlock"); + for (const auto& [present, feature] : {std::pair{context.storageImageMultisample, "shaderStorageImageMultisample"}, std::pair{context.sampleRateShading, "sampleRateShading"}, std::pair{TargetHasCapability(device->Target(), spv::CapabilityFragmentShaderPixelInterlockEXT), "fragmentShaderPixelInterlock"}}) { + if (present) continue; + std::printf("skipped, the device lacks %s\n", feature); return VulkanTestSkipped; } Block block(4u * BlockBytes); const auto color = DecodeTarget(block); - const auto full = TargetState(color, 0xfu); + const auto full = TargetState(block, 0xfu); Draw(*device, full, Covering, Float32Program(First)); - const auto merged = TargetState(color, 0x1u); + const auto merged = TargetState(block, 0x1u); Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); Draw(*device, merged, Cutting, Float32Program(Second)); Draw(*device, full, Overlapping, Float32Program(Third)); From 6f0873cde0795d707712eb043001459fa56640f3 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Sun, 18 Oct 2026 19:20:26 +0200 Subject: [PATCH 059/318] docs(agc): list the limits of merged 2-sample 4_4_4_4 partial writes Per-sample shading of a merging draw makes centroid and sample-located inputs and the ancillary sample number follow the invocation's sample (centre-located inputs too on devices without interpolation functions and fragment shader barycentrics), and the 2-sample merge was measured on one GPU only. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ee73a1660c..4868c8e36d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -160,9 +160,10 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. +- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `CB_TARGET_MASK` that leaves out some components of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample and asks the recompiler for an iterated shader. Centre-located positions stay at the pixel centre, and so do centre-located inputs on devices with interpolation functions and fragment shader barycentrics (elsewhere they move to the sample position); centroid inputs, sample-located inputs and positions, and the sample number in the ancillary input follow the invocation's sample instead of the pixel's centroid and sample 0. A guest shader written for pixel-rate shading can see slightly different values there. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. -- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444PartialWrite.cpp) (libSceAgcDriver) - the single-sample merge is measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. +- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample and 2-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. From 3e8f7972074a020c1a63d392d77cbe7a8370ca24 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:53:52 +0200 Subject: [PATCH 060/318] fix(agc): refuse coverage input in a merged multisampled 4_4_4_4 draw A partial write of a multisampled 4_4_4_4 target forces per-sample shading after DecodeState checked SAMPLE_COVERAGE against the guest's own iteration, so a pixel-rate shader reading SAMPLE_COVERAGE got only its invocation's sample in gl_SampleMaskIn instead of the whole pixel's coverage. The forced shading now runs the same check and throws its message; a full write of the same target still decodes. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 5 ++++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 5fa91dcea9..557f8e262d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1474,7 +1474,10 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { Require(std::ranges::none_of(result.colors, [](const ColorTarget& other) { return other.partialWriteMask != 0; }), "partial writes of more than one packed color target are not implemented"); color.partialWriteMask = PackedWriteMask(mapping, exportedMask); state.colorWriteMask = 0; - if (color.samples > 1u) result.perSampleShading = true; + if (color.samples > 1u) { + Require(sampleCoverageSupported(queue, result.samples, true), "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample, or reads sample-rate or ancillary inputs, is unsupported (the input holds the whole pixel's coverage, gl_SampleMaskIn only the invocation's samples)"); + result.perSampleShading = true; + } } } if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 79d3ed27dd..c8b7265de4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9001,6 +9001,12 @@ void unorm4_4_4_4TargetTests() { { const auto multisampled = AgcDriver::Graphics::DecodeState(changed); Require(multisampled.samples == 2u && multisampled.color.samples == 2u && multisampled.color.partialWriteMask == 0xf000u && multisampled.blends[0].colorWriteMask == 0u && multisampled.perSampleShading, "a partial write of a 2-sample 4_4_4_4 unorm target did not merge per sample behind a closed write mask"); + auto coverage = changed; + coverage.context[0x1b3] = 0x4002u; + coverage.context[0x1b4] = 0x4002u; + expectFailure([&] { AgcDriver::Graphics::DecodeState(coverage); }, "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample"); + coverage.context[0x8e] = 0xfu; + Require(!AgcDriver::Graphics::DecodeState(coverage).perSampleShading, "a full write of a 2-sample 4_4_4_4 unorm target with a SAMPLE_COVERAGE input was refused or ran per sample"); const auto merge = AgcDriver::Graphics::ExportTargetMerge(multisampled); const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); Require(merge.samples == 2u && merge.mask == 0xf000u && surface.dimension == AgcDriver::Graphics::TextureDimension::k2DMsaa && surface.samples == 2u && surface.baseAddress == multisampled.color.surfaceAddress && surface.width == multisampled.color.extent.width && surface.height == multisampled.color.extent.height && surface.format == AgcDriver::Graphics::TargetMergeSurface(multisampled.color).format, "the merge descriptor of a 2-sample 4_4_4_4 unorm target does not decode to its multisampled surface"); From a1814c5853d173bdec36b5a2158feaac3e7f769d Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:58:44 +0200 Subject: [PATCH 061/318] fix(agc): refuse per-pixel inputs that move when a 2-sample merge iterates A partial write of a multisampled 4_4_4_4 target makes the draw shade per sample. For a shader the guest runs per pixel, every invocation of a pixel must then see the same inputs. Otherwise its colours and kill decisions follow each sample, while the console runs one invocation. The decode now throws in two cases when the guest did not ask for per-sample shading. The first is a shader that reads the ancillary word, whose sample number would become the invocation's. The second is a shader that reads centroid, sample-located or pull-model barycentrics, Z, 1/W, or a position at the centroid or a sample. That case is allowed when every sample sits at the pixel centre, as in a draw raised from a one-sample rasterizer. The recompiler throws for centre-located barycentrics on a device without interpolation functions and fragment shader barycentrics, where they would not be rebuilt at the centre. Decode tests run each moving input on a 2-sample rasterizer, where it throws, and with PS_ITER_SAMPLE, where it decodes. The raised draw keeps the located inputs and refuses the ancillary word. Centre and upper-left positions, centre barycentrics, the front face and the fixed-point position decode, and a recompile test covers the missing barycentrics. The debt entry now lists what is kept and what throws. Co-Authored-By: Claude Sonnet 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 25 ++++++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 38 ++++++++++++++++++- .../Translation/src/InstructionTranslator.cpp | 3 ++ docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 557f8e262d..827db079ca 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -505,6 +505,25 @@ bool sampleCoverageSupported(const QueueState& queue, std::uint32_t samples, boo return (inputs & PixelSampleCoverageInput) == 0u || (!perSampleShading && (inputs & PixelSampleRateInputs) == 0u); } +bool pixelRateInputsKept(const QueueState& queue, const State& state) { + using ShaderRecompiler::PixelInput; + using ShaderRecompiler::PixelInputBit; + constexpr std::uint32_t numbered = PixelInputBit(PixelInput::Ancillary) | PixelInputBit(PixelInput::SampleCoverage); + constexpr std::uint32_t located = PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::PerspectiveCentroid) | PixelInputBit(PixelInput::PerspectivePullModel) | PixelInputBit(PixelInput::LinearSample) | PixelInputBit(PixelInput::LinearCentroid) | PixelInputBit(PixelInput::PositionZ) | PixelInputBit(PixelInput::PositionW); + const bool centred = state.customSampleLocations && std::ranges::all_of(state.sampleLocations, [](std::uint8_t location) { return location == 0u; }); + const auto moving = numbered | (centred ? 0u : located); + if (PixelProgramSkipped(queue)) return true; + const auto ena = find(queue.context, 0x1b3); + const auto addr = find(queue.context, 0x1b4); + if (ena == queue.context.end() || addr == queue.context.end()) return true; + const auto inputs = ena->second & addr->second; + if ((inputs & moving) != 0u) return false; + if (centred || (inputs & (PixelInputBit(PixelInput::PositionX) | PixelInputBit(PixelInput::PositionY))) == 0u) return true; + const auto position = find(queue.context, 0x1b8); + const auto control = position == queue.context.end() ? 0u : position->second; + return (control & 0x100000u) != 0u || ((control >> 16u) & 3u) == 0u; +} + bool coveredCentroidSupported(const QueueState& queue) { const auto config = find(queue.context, 0x2f8); if (config == queue.context.end() || (config->second & CoveredCentroidIsCenter) == 0 || PixelProgramSkipped(queue)) return true; @@ -1476,6 +1495,12 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { state.colorWriteMask = 0; if (color.samples > 1u) { Require(sampleCoverageSupported(queue, result.samples, true), "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample, or reads sample-rate or ancillary inputs, is unsupported (the input holds the whole pixel's coverage, gl_SampleMaskIn only the invocation's samples)"); + if (!result.perSampleShading && !pixelRateInputsKept(queue, result)) { + const auto value = [&](std::uint32_t offset) { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; }; + char detail[160]; + std::snprintf(detail, sizeof(detail), " (SPI_PS_INPUT_ENA 0x%x, SPI_PS_INPUT_ADDR 0x%x, SPI_BARYC_CNTL 0x%x, sample 1 location 0x%02x)", value(0x1b3), value(0x1b4), value(0x1b8), result.sampleLocations[1]); + Require(false, std::string("partial writes of a multisampled 4_4_4_4 unorm color target run the pixel shader once per sample, so a shader the guest runs once per pixel cannot read the ancillary word or, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W or a position at the centroid or a sample: they would follow the invocation's sample") + detail); + } result.perSampleShading = true; } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index c8b7265de4..35d32755ca 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9007,6 +9007,36 @@ void unorm4_4_4_4TargetTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(coverage); }, "SAMPLE_COVERAGE in a multisampled pixel shader that runs per sample"); coverage.context[0x8e] = 0xfu; Require(!AgcDriver::Graphics::DecodeState(coverage).perSampleShading, "a full write of a 2-sample 4_4_4_4 unorm target with a SAMPLE_COVERAGE input was refused or ran per sample"); + struct InputCase { + std::uint32_t inputs; + std::uint32_t position; + const char* what; + }; + auto rasterized = changed; + rasterized.context[0x2f8] = 0x00100001u; + rasterized.context[0x292] = 3; + rasterized.context[0x3b8] = 0x0dc00000u; + for (std::uint32_t pixel = 0; pixel < 4; ++pixel) rasterized.context[0x2fe + pixel * 4u] = 0xcc44; + const auto inputState = [](AgcDriver::QueueState queue, std::uint32_t inputs, std::uint32_t position) { + queue.context[0x1b3] = inputs; + queue.context[0x1b4] = inputs; + queue.context[0x1b8] = position; + return queue; + }; + for (const auto& [inputs, position, what] : {InputCase{0x0004u, 0u, "PERSP_CENTROID"}, InputCase{0x0040u, 0u, "LINEAR_CENTROID"}, InputCase{0x0001u, 0u, "PERSP_SAMPLE"}, InputCase{0x0008u, 0u, "PERSP_PULL_MODEL"}, InputCase{0x2002u, 0u, "ANCILLARY"}, InputCase{0x0402u, 0u, "POS_Z_FLOAT"}, InputCase{0x0802u, 0u, "POS_W_FLOAT"}, InputCase{0x0302u, 0x20000u, "POS_X_FLOAT and POS_Y_FLOAT at a sample"}, InputCase{0x0102u, 0x10000u, "POS_X_FLOAT at the centroid"}}) { + auto moved = inputState(rasterized, inputs, position); + expectFailure([&] { AgcDriver::Graphics::DecodeState(moved); }, "cannot read the ancillary word or, unless every sample sits at the pixel centre"); + moved.context[0x293] = moved.context.at(0x293) | 0x10000u; + Require(AgcDriver::Graphics::DecodeState(moved).perSampleShading, std::string("a partial write of a 2-sample 4_4_4_4 unorm target whose shader the guest runs per sample was refused for reading ") + what); + if (inputs == 0x2002u) { + expectFailure([&] { AgcDriver::Graphics::DecodeState(inputState(changed, inputs, position)); }, "cannot read the ancillary word"); + continue; + } + Require(AgcDriver::Graphics::DecodeState(inputState(changed, inputs, position)).perSampleShading, std::string("a partial write of a 2-sample 4_4_4_4 unorm target raised from a one-sample rasterizer, every sample at the pixel centre, was refused for reading ") + what); + } + for (const auto& [inputs, position, what] : {InputCase{0x0302u, 0u, "POS_X_FLOAT and POS_Y_FLOAT at the centre"}, InputCase{0x0302u, 0x100000u, "POS_X_FLOAT and POS_Y_FLOAT at the upper-left corner"}, InputCase{0x9022u, 0u, "PERSP_CENTER, LINEAR_CENTER, FRONT_FACE and POS_FIXED_PT"}}) { + Require(AgcDriver::Graphics::DecodeState(inputState(rasterized, inputs, position)).perSampleShading, std::string("a partial write of a 2-sample 4_4_4_4 unorm target whose pixel-rate shader reads ") + what + " was refused"); + } const auto merge = AgcDriver::Graphics::ExportTargetMerge(multisampled); const auto surface = AgcDriver::Graphics::DecodeTextureResource(merge.descriptor); Require(merge.samples == 2u && merge.mask == 0xf000u && surface.dimension == AgcDriver::Graphics::TextureDimension::k2DMsaa && surface.samples == 2u && surface.baseAddress == multisampled.color.surfaceAddress && surface.width == multisampled.color.extent.width && surface.height == multisampled.color.extent.height && surface.format == AgcDriver::Graphics::TargetMergeSurface(multisampled.color).format, "the merge descriptor of a 2-sample 4_4_4_4 unorm target does not decode to its multisampled surface"); @@ -9016,8 +9046,9 @@ void unorm4_4_4_4TargetTests() { constexpr std::array mergeCode{0xf800180fu, 0x07060504u, 0xbf810000u}; auto merging = packedExportRequest(changed, multisampled, mergeCode); merging.context.pixel->targetMerge = merge; - const std::array capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityStorageImageMultisample}; + const std::array capabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityStorageImageMultisample, spv::CapabilityInterpolationFunction}; merging.target.supportedCapabilities = capabilities; + merging.target.fragmentShaderBarycentricEnabled = true; const auto words = ShaderRecompiler::Recompile(merging).spirv.Copy(); std::size_t sampled = 0; bool sampleRate = false; @@ -9029,9 +9060,12 @@ void unorm4_4_4_4TargetTests() { } Require(sampleRate && sampled == 2u && interlockShape(words) == "capability extension ordered begin end ", "a 2-sample 4_4_4_4 merge module does not read and write the shaded sample inside a pixel interlock"); auto single = merging; - const std::array noMultisampleStorage{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat}; + const std::array noMultisampleStorage{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityInterpolationFunction}; single.target.supportedCapabilities = noMultisampleStorage; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(single)); }, "storage image multisampling"); + auto offCentre = merging; + offCentre.target.fragmentShaderBarycentricEnabled = false; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(offCentre)); }, "its centre-located barycentrics would follow the sample"); } for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { changed = queue; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index d73689d528..8626dbc6ae 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -367,6 +367,9 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO program.Metadata().pixelIteratesSamples = iterated; const bool offsets = options.interpolationFunctions && options.fragmentShaderBarycentricEnabled; const bool centers = iterated && offsets; + if (ps->mergeSamples > 1u && !offsets && (loaded(PixelInput::PerspectiveCenter) || loaded(PixelInput::LinearCenter))) { + throw std::runtime_error("a partial write of a multisampled packed color target runs the pixel shader once per sample, and without interpolation functions and fragment shader barycentrics its centre-located barycentrics would follow the sample"); + } const bool sampleOffsets = offsets && ps->SampleLocationsOffCenter(); const auto smoothSample = sampleOffsets ? StageInputKind::BaryCoordSmoothSample : StageInputKind::BaryCoordSmooth; const auto linearSample = sampleOffsets ? StageInputKind::BaryCoordNoPerspectiveSample : StageInputKind::BaryCoordNoPerspective; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 4868c8e36d..bef52a2754 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -160,7 +160,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. -- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `CB_TARGET_MASK` that leaves out some components of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample and asks the recompiler for an iterated shader. Centre-located positions stay at the pixel centre, and so do centre-located inputs on devices with interpolation functions and fragment shader barycentrics (elsewhere they move to the sample position); centroid inputs, sample-located inputs and positions, and the sample number in the ancillary input follow the invocation's sample instead of the pixel's centroid and sample 0. A guest shader written for pixel-rate shading can see slightly different values there. +- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `CB_TARGET_MASK` that leaves out some components of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample and asks the recompiler for an iterated shader. For a shader the guest runs once per pixel this keeps only the inputs that every sample invocation of a pixel sees alike: centre-located barycentrics (rebuilt at the centre with interpolation functions and fragment shader barycentrics; without them the draw throws), positions at the centre or the upper-left corner, the fixed-point position and the front face, so each invocation reaches the colour and kill decision of the console's single invocation. The draw throws when such a shader reads the ancillary word, whose sample number would be the invocation's, or, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W or a position at the centroid or a sample. Not measured on a PS5. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. - [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample and 2-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. From d2a5895b8d1af74bec1eed1fc3b0f9b9b6f7c7dd Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:58:57 +0200 Subject: [PATCH 062/318] fix(agc): refuse 2-sample 4_4_4_4 merges of shaders that write memory A merge into a multisampled 4_4_4_4 target runs the pixel shader once per sample. A shader that stores to guest memory, runs atomics or uses GDS would then repeat those writes for every covered sample. An 8-sample pixel would add to a counter eight times. Such a merge now throws. The recompile request carries the merge's sample count, not whether the guest asked for per-sample shading, so this also refuses shaders the guest iterates itself, where the console repeats the writes too. A recompile test checks that a 2-sample merging shader adding to a buffer counter throws, and that the same shader without the merge compiles. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 16 ++++++++++++++++ .../src/SpirvMemory/SpirvModuleSetup.cpp | 3 +++ docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 35d32755ca..0ce922005a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9066,6 +9066,22 @@ void unorm4_4_4_4TargetTests() { auto offCentre = merging; offCentre.target.fragmentShaderBarycentricEnabled = false; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(offCentre)); }, "its centre-located barycentrics would follow the sample"); + alignas(16) static std::array counted{}; + static const std::array counter{static_cast(reinterpret_cast(counted.data())), static_cast(reinterpret_cast(counted.data()) >> 32u) & 0xffffu, 16u, 0x31016facu}; + constexpr std::array countingCode{0x7e000281u, 0xe0c80000u, 0x80000000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; + auto counting = packedExportRequest(changed, multisampled, countingCode); + counting.context.userData = counter; + counting.context.pixel->targetMerge = merge; + counting.context.pixel->iteratesSamples = true; + counting.context.pixel->earlyZ = true; + counting.target = merging.target; + counting.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + static const std::array atomicCapabilities{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityStorageImageMultisample, spv::CapabilityInterpolationFunction, spv::CapabilityInt64}; + counting.target.supportedCapabilities = atomicCapabilities; + expectFailure([&] { static_cast(ShaderRecompiler::Recompile(counting)); }, "each covered sample would repeat its writes"); + auto unmergedCounting = counting; + unmergedCounting.context.pixel->targetMerge = {}; + static_cast(ShaderRecompiler::Recompile(unmergedCounting)); } for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 2u}, std::pair{0u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) { changed = queue; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 0f95c839c2..d1193cc5f7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -406,6 +406,9 @@ void DefineModule(SpirvEmitterState& state) { if (merge && (state.depthVariable != 0 || state.stencilRefVariable != 0 || pixel.psDepthExportEnable || pixel.psSampleMaskExportEnable || pixel.psStencilRefExportEnable || pixel.psAlphaToCoverage || pixel.QuadMasked() || (pixel.psPixelKillEnable && !pixel.psEarlyZ && pixel.mergeDepthStencilWrites))) { throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that kills pixels under late tests of a draw writing depth or stencil, exports depth, stencil or coverage, or runs with a quad sample mask cannot use"); } + if (merge && pixel.mergeSamples > 1u && WritesGuestMemory(state.program)) { + throw std::runtime_error("a partial write of a multisampled packed color target runs the pixel shader once per sample, which a shader that writes guest memory (stores, atomics or GDS) cannot use: each covered sample would repeat its writes"); + } if (merge && !pixel.psEarlyZ && pixel.mergeDepthStencilTests && WritesGuestMemory(state.program)) { throw std::runtime_error("a partial write of a packed color target merges inside the pixel shader after early depth and stencil tests, which a shader that writes guest memory (stores, atomics or GDS) under late tests cannot use: the pixels the tests reject would still write it"); } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index bef52a2754..47e51606ff 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -160,7 +160,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. -- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a `CB_TARGET_MASK` that leaves out some components of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample and asks the recompiler for an iterated shader. For a shader the guest runs once per pixel this keeps only the inputs that every sample invocation of a pixel sees alike: centre-located barycentrics (rebuilt at the centre with interpolation functions and fragment shader barycentrics; without them the draw throws), positions at the centre or the upper-left corner, the fixed-point position and the front face, so each invocation reaches the colour and kill decision of the console's single invocation. The draw throws when such a shader reads the ancillary word, whose sample number would be the invocation's, or, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W or a position at the centroid or a sample. Not measured on a PS5. +- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample. For a shader the guest runs per pixel it throws on inputs that would follow the invocation's sample: the ancillary word and, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W and positions at the centroid or a sample; centre barycentrics throw without interpolation functions and fragment shader barycentrics. Centre barycentrics and positions, upper-left and fixed-point positions and the front face are kept, so each invocation reaches the console's colour and kill decision. A shader that writes guest memory throws, as each covered sample would repeat the writes, also when the guest iterates it itself (the request does not say). Not measured on a PS5. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. - [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample and 2-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. From c21356d42adb8ad1ecbb350acdee8d341205806b Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:58:57 +0200 Subject: [PATCH 063/318] test(agc): draw two 2-sample 4_4_4_4 merges back to back The 2-sample test now draws two merges back to back with no read-back in between: an X merge over one triangle and a W merge over a crossing one. It then loads every sample. Samples inside both triangles must hold both nibbles, so the second merge has to read the word the first one wrote. Each merging draw takes the target's storage view before it records. That view ends any open render pass and records a shader-write barrier, so two merges never share a pass. Co-Authored-By: Claude Sonnet 5.5 --- ...PackedUnorm4444MultisamplePartialWrite.cpp | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp index b5fd107ed5..14850a0815 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -56,6 +56,7 @@ constexpr double EdgeMargin = 1.0 / 32.0; constexpr std::array First{0.2f, 0.4f, 0.6f, 0.8f}; constexpr std::array Second{13.0f / 15.0f, 0.0f, 1.0f, 0.0f}; constexpr std::array Third{1.0f / 15.0f, 1.0f, 0.0f, 4.0f / 15.0f}; +constexpr std::array Fourth{0.0f, 7.0f / 15.0f, 0.0f, 11.0f / 15.0f}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -76,11 +77,15 @@ constexpr std::array, 3> Cutting{{ {-0.93f, -1.0f, 0.5f, 1.0f}, {0.37f, -1.0f, 0.5f, 1.0f}, {-0.21f, 0.94f, 0.5f, 1.0f} }}; +constexpr std::array, 3> Crossing{{ + {-0.6f, -0.2f, 0.5f, 1.0f}, {0.9f, -0.85f, 0.5f, 1.0f}, {0.5f, 0.9f, 0.5f, 1.0f} +}}; + constexpr std::array, 3> Overlapping{{ {-0.08f, -0.47f, 0.5f, 1.0f}, {1.0f, -0.13f, 0.5f, 1.0f}, {0.29f, 1.0f, 0.5f, 1.0f} }}; -alignas(256) std::array, 4> Programs{}; +alignas(256) std::array, 8> Programs{}; std::size_t programCount = 0; alignas(256) std::array Buffer{}; @@ -186,7 +191,7 @@ AgcDriver::Graphics::State TargetState(const Block& block, std::uint32_t exporte const auto& color = decoded.colors.front(); char detail[256]; std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x of the 2-sample 4_4_4_4 target decoded %zu targets, %u samples, word mask 0x%04x, write mask 0x%x and %s shading", exportedMask, decoded.colors.size(), decoded.samples, color.partialWriteMask, decoded.blends.front().colorWriteMask, decoded.perSampleShading ? "per-sample" : "pixel-rate"); - Require(decoded.colors.size() == 1u && decoded.samples == Samples && color.samples == Samples && color.partialWriteMask == (partial ? 0xf000u : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); + Require(decoded.colors.size() == 1u && decoded.samples == Samples && color.samples == Samples && color.partialWriteMask == (partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); const auto merge = AgcDriver::Graphics::ExportTargetMerge(decoded); Require(merge.mask == color.partialWriteMask && merge.samples == (partial ? Samples : 1u) && !merge.depthStencilWrites && merge.descriptor == (partial ? AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)) : std::array{}), "the decoded 2-sample target exported another merge than its own surface"); AgcDriver::Graphics::State state{}; @@ -358,7 +363,36 @@ int main() { Require(checked >= Side * Side * Samples * 8u / 10u, "fewer than eight in ten samples are clear of the triangles' edges"); Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0 && pixelsByCoverage[1] != 0, "the merged samples do not leave pixels unmerged, merged and partly merged"); Require(overdrawn != 0 && mergedAfterOverdraw != 0, "the last full write does not cover some merged samples and leave others"); - std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, and a full write drawn right after covered %u merged samples\n", pixelsByCoverage[1], overdrawn); + Draw(*device, full, Covering, Float32Program(First)); + const auto lowMerged = TargetState(block, 0x8u); + Require(lowMerged.colors.front().partialWriteMask == 0x000fu, "a W write of the 4_4_4_4 target did not merge bits 3..0"); + Draw(*device, merged, Cutting, Float32Program(Second)); + Draw(*device, lowMerged, Crossing, Float32Program(Fourth)); + const auto twice = LoadSamples(*device, texture); + std::uint32_t checkedTwice = 0; + std::uint32_t mergedTwice = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + for (std::uint32_t sample = 0; sample < Samples; ++sample) { + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto high = Inside(Cutting, px, py); + const auto low = Inside(Crossing, px, py); + if (std::fabs(high) < EdgeMargin || std::fabs(low) < EdgeMargin) continue; + ++checkedTwice; + mergedTwice += high > 0 && low > 0 ? 1u : 0u; + auto expected = background; + if (high > 0) expected = (expected & 0x0fffu) | (Packed(Second) & 0xf000u); + if (low > 0) expected = (expected & 0xfff0u) | (Packed(Fourth) & 0x000fu); + const auto word = twice[(y * Side + x) * Samples + sample]; + char where[192]; + std::snprintf(where, sizeof(where), "after an X merge and a W merge drawn back to back, sample %u of pixel (%u, %u) holds 0x%04x, expected 0x%04x", sample, x, y, word, expected); + Require(word == expected, where); + } + } + } + Require(checkedTwice >= Side * Side * Samples * 8u / 10u && mergedTwice != 0, "the back-to-back merges leave fewer than eight in ten samples clear of their edges, or no sample inside both"); + std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, a full write drawn right after covered %u merged samples, and %u samples kept both of two merges drawn back to back\n", pixelsByCoverage[1], overdrawn, mergedTwice); return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 69be01d8432cb87fc5a3c2fd276f5fa0e3eb13ed Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:36:17 +0200 Subject: [PATCH 064/318] fix(agc): keep centre barycentrics of 2-sample merges whose samples sit at the centre The recompiler refused centre-located barycentrics in a multisampled merge on a device without interpolation functions and fragment shader barycentrics. It did so even when every sample sits at the pixel centre, as in a draw raised from a one-sample rasterizer, where the barycentrics are the centre's anyway. The refusal now applies only when the merge's sample locations are off the centre. The pixel request carries those locations for such a merge. The recompile test runs on two states. On a 2-sample rasterizer, the missing barycentrics throw, and the shader compiles once they are present. On a raised draw, the shader compiles without them. Co-Authored-By: Claude Sonnet 5.5 --- .../prx/libSceAgcDriver/Graphics/src/State.cpp | 3 ++- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 15 ++++++++++++++- .../Translation/src/InstructionTranslator.cpp | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 827db079ca..338e39ae82 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -974,7 +974,8 @@ std::array PixelSampleLocations(const State& state, const Shad std::array locations{}; const bool sampleBarycentrics = (pixel.inputAddr & (PixelInputBit(PixelInput::PerspectiveSample) | PixelInputBit(PixelInput::LinearSample))) != 0u; const bool samplePosition = !state.perSampleShading && (pixel.posX || pixel.posY) && pixel.positionLocation == ShaderRecompiler::PixelPositionLocation::Sample; - if (!sampleBarycentrics && !samplePosition) return locations; + const bool mergedCentres = pixel.targetMerge.samples > 1u && (pixel.inputAddr & (PixelInputBit(PixelInput::PerspectiveCenter) | PixelInputBit(PixelInput::LinearCenter))) != 0u; + if (!sampleBarycentrics && !samplePosition && !mergedCentres) return locations; const auto samples = state.perSampleShading ? state.samples : 1u; for (std::uint32_t pixelIndex = 0; pixelIndex < 4u; ++pixelIndex) { for (std::uint32_t sample = 0; sample < samples; ++sample) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 0ce922005a..fcba30f695 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9063,9 +9063,22 @@ void unorm4_4_4_4TargetTests() { const std::array noMultisampleStorage{spv::CapabilityShader, spv::CapabilityFragmentShaderPixelInterlockEXT, spv::CapabilityStorageImageReadWithoutFormat, spv::CapabilityInterpolationFunction}; single.target.supportedCapabilities = noMultisampleStorage; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(single)); }, "storage image multisampling"); - auto offCentre = merging; + auto centred = merging; + centred.target.fragmentShaderBarycentricEnabled = false; + centred.context.pixel->sampleLocations = AgcDriver::Graphics::PixelSampleLocations(multisampled, *centred.context.pixel); + Require(std::ranges::all_of(centred.context.pixel->sampleLocations, [](std::uint8_t location) { return location == 0u; }), "a merge raised from a one-sample rasterizer did not keep every sample at the pixel centre"); + static_cast(ShaderRecompiler::Recompile(centred)); + const auto rasterizedState = AgcDriver::Graphics::DecodeState(rasterized); + auto offCentre = packedExportRequest(rasterized, rasterizedState, mergeCode); + offCentre.context.pixel->targetMerge = AgcDriver::Graphics::ExportTargetMerge(rasterizedState); + offCentre.context.pixel->iteratesSamples = rasterizedState.perSampleShading; + offCentre.context.pixel->sampleLocations = AgcDriver::Graphics::PixelSampleLocations(rasterizedState, *offCentre.context.pixel); + Require(std::ranges::any_of(offCentre.context.pixel->sampleLocations, [](std::uint8_t location) { return location != 0u; }), "a 2-sample rasterizer's merge did not pass its off-centre sample locations"); + offCentre.target.supportedCapabilities = capabilities; offCentre.target.fragmentShaderBarycentricEnabled = false; expectFailure([&] { static_cast(ShaderRecompiler::Recompile(offCentre)); }, "its centre-located barycentrics would follow the sample"); + offCentre.target.fragmentShaderBarycentricEnabled = true; + static_cast(ShaderRecompiler::Recompile(offCentre)); alignas(16) static std::array counted{}; static const std::array counter{static_cast(reinterpret_cast(counted.data())), static_cast(reinterpret_cast(counted.data()) >> 32u) & 0xffffu, 16u, 0x31016facu}; constexpr std::array countingCode{0x7e000281u, 0xe0c80000u, 0x80000000u, 0xf800180fu, 0x07060504u, 0xbf810000u}; diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index 8626dbc6ae..bae160af93 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -367,7 +367,7 @@ void emitEntryPrologue(IrProgram& program, IrBlock& entryBlock, const TranslateO program.Metadata().pixelIteratesSamples = iterated; const bool offsets = options.interpolationFunctions && options.fragmentShaderBarycentricEnabled; const bool centers = iterated && offsets; - if (ps->mergeSamples > 1u && !offsets && (loaded(PixelInput::PerspectiveCenter) || loaded(PixelInput::LinearCenter))) { + if (ps->mergeSamples > 1u && !offsets && ps->SampleLocationsOffCenter() && (loaded(PixelInput::PerspectiveCenter) || loaded(PixelInput::LinearCenter))) { throw std::runtime_error("a partial write of a multisampled packed color target runs the pixel shader once per sample, and without interpolation functions and fragment shader barycentrics its centre-located barycentrics would follow the sample"); } const bool sampleOffsets = offsets && ps->SampleLocationsOffCenter(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 47e51606ff..e52b45acc2 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -160,7 +160,7 @@ - [8_SRGB color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) on such a device throw unless they are single-mip 2D surfaces with a resident image; linear targets have none. - [5_6_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R5G6B5. The field holding X is in bits 0-4, Y in 5-10 and Z in 11-15, as for the other AMD formats; the component swap picks which export fills them, following `ac_translate_colorswap` in [Mesa 26.2.2 ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c), which programs `SWAP_STD_REV` (2) for a surface with red in bits 11-15. With `SWAP_STD_REV` red goes to bits 11-15, R5G6B5's red, so the exports are written as they are; with `SWAP_STD` (0) red goes to bits 0-4 and the exports are written with X and Z exchanged, as for 1_5_5_5. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets in both orders and compares every stored texel with that layout on the host device; the export direction is not measured on a PS5. Only UNORM with those two component orders is accepted; `SWAP_ALT` and `SWAP_ALT_REV` keep throwing. Destination alpha reads as 1, as Vulkan defines it for a format without alpha, which is not measured on the console. Constant blend colors with `SWAP_STD` go through the `0xc6` component-swap handling, which is not tested for this format. R5G6B5 has no entry in the DCC and multisample clear-encoding tables, so a CB DCC decompress or eliminate of DCC clear codes other than 0000 and a fast clear of a multisampled 5_6_5 target throw, as for 1_5_5_5. PPSA27305 renders into this format with `SWAP_STD_REV` (discussion #6434). - [1_5_5_5 color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into A1R5G5B5. AMD formats keep the first component in the least significant bits (X in bits 0-4, Y in 5-9, Z in 10-14, W in bit 15, as in the [fpPS4](https://github.com/red-prig/fpPS4) `kSurfaceFormat1_5_5_5` doc comment and as measured for textures in the packed 16-bit textures entry), while A1R5G5B5 keeps R in bits 10-14 and B in bits 0-4, so the pixel exports are written with X and Z exchanged, which makes the stored bits follow the guest layout. The decoded format, component mapping and write mask are tested, and an execution test draws 32_ABGR exports into tiled and linear targets and compares every stored texel with the guest layout on the host device; the export direction is not measured on a PS5. Only UNORM with the standard component swap is accepted. Constant blend colors go through the same component-swap handling as other targets with that export mapping (`0xc6`), which is not tested for this format. PPSA06699 renders into this format. -- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample. For a shader the guest runs per pixel it throws on inputs that would follow the invocation's sample: the ancillary word and, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W and positions at the centroid or a sample; centre barycentrics throw without interpolation functions and fragment shader barycentrics. Centre barycentrics and positions, upper-left and fixed-point positions and the front face are kept, so each invocation reaches the console's colour and kill decision. A shader that writes guest memory throws, as each covered sample would repeat the writes, also when the guest iterates it itself (the request does not say). Not measured on a PS5. +- [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample. For a shader the guest runs per pixel it throws on inputs that would follow the invocation's sample: the ancillary word and, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W and positions at the centroid or a sample; off-centre samples make centre barycentrics throw without interpolation functions and fragment shader barycentrics. Centre barycentrics and positions, upper-left and fixed-point positions and the front face are kept, so each invocation reaches the console's colour and kill decision. A shader that writes guest memory throws, as each covered sample would repeat the writes, also when the guest iterates it itself (the request does not say). Not measured on a PS5. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. - [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample and 2-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. From f5edd537e51b1bac4c2ce203e0509b54709bbe8a Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:46:10 +0200 Subject: [PATCH 065/318] test(agc): merge partial 4_4_4_4 writes into 4- and 8-sample targets The multisampled merge accepts 2, 4 and 8 samples, but the execution test drew only 2-sample targets. It now runs the same draws on 2, 4 and 8 samples, each count with its own target: a cutting merge under a full write, then an X and a W merge drawn back to back. Every sample is read back through a T# of the target's sample count and checked against the standard sample locations. A partly merged pixel is now any pixel with some, but not all, of its samples merged. A count whose R16_UINT colour attachment with storage use the device does not offer is skipped and printed. The test is skipped when the device offers none of the three. The debt entry on measured hardware now names the 4- and 8-sample merges. Co-Authored-By: Claude Sonnet 5.5 --- ...PackedUnorm4444MultisamplePartialWrite.cpp | 208 ++++++++++-------- docs/dev/debt/libSceAgcDriver.md | 2 +- 2 files changed, 118 insertions(+), 92 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp index 14850a0815..894ccc375a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include #include @@ -42,11 +44,9 @@ using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Side = 16; -constexpr std::uint32_t Samples = 2; -constexpr std::uint32_t SampleShift = 1; +constexpr std::array SampleCounts{2, 4, 8}; constexpr std::size_t BlockBytes = 65536; constexpr std::uint32_t Unorm4_4_4_4Info = 0x8000u | (19u << 2u) | (2u << 11u); -constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t Float32Export = 9; constexpr std::uint32_t Threads = 32; @@ -85,10 +85,15 @@ constexpr std::array, 3> Overlapping{{ {-0.08f, -0.47f, 0.5f, 1.0f}, {1.0f, -0.13f, 0.5f, 1.0f}, {0.29f, 1.0f, 0.5f, 1.0f} }}; -alignas(256) std::array, 8> Programs{}; +alignas(256) std::array, 24> Programs{}; std::size_t programCount = 0; alignas(256) std::array Buffer{}; +std::uint32_t MultisampledAttrib(std::uint32_t samples) { + const auto shift = static_cast(std::countr_zero(samples)); + return (shift << 12u) | (shift << 15u); +} + std::uint32_t Unorm(float value) { if (!(value > 0.0f)) return 0; if (value >= 1.0f) return 15; @@ -143,21 +148,21 @@ struct Block { std::uint8_t* data = nullptr; }; -AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block) { +AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t samples) { AgcDriver::Registers cx; cx[0x318] = static_cast(block.Address() >> 8u); cx[0x31b] = 0; cx[0x31c] = Unorm4_4_4_4Info; - cx[0x31d] = MultisampledAttrib; + cx[0x31d] = MultisampledAttrib(samples); cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); cx[0x3b8] = TiledAttrib3; cx[0x390] = static_cast(block.Address() >> 40u); const auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); - Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 packed R16_UINT surface of 2 samples"); + Require(color.format == VK_FORMAT_R16_UINT && color.componentMapping == 0x1bu && color.packing == ShaderRecompiler::ColorExportPacking::Unorm4_4_4_4 && color.samples == samples && color.extent.width == Side && color.extent.height == Side, "the target did not decode as a 16x16 packed R16_UINT surface of " + std::to_string(samples) + " samples"); return color; } -AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask) { +AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask, std::uint32_t samples) { AgcDriver::QueueState queue; queue.userConfig[0x242] = 4; queue.context = { @@ -169,7 +174,7 @@ AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask) { {0x8e, targetMask}, {0x8f, 0xf}, {0x202, 0xcc0010}, {0x1c4, 0}, {0x1c5, Float32Export}, {0x1c3, 4}, {0x318, static_cast(block.Address() >> 8u)}, {0x390, static_cast(block.Address() >> 40u)}, - {0x31b, 0}, {0x31c, Unorm4_4_4_4Info}, {0x31d, MultisampledAttrib}, + {0x31b, 0}, {0x31c, Unorm4_4_4_4Info}, {0x31d, MultisampledAttrib(samples)}, {0x3b0, ((Side - 1u) << 14u) | (Side - 1u)}, {0x3b8, TiledAttrib3}, {0x1e0, 0}, {0xc, 0}, {0xd, (Side << 16u) | Side}, {0x81, 0x80000000}, {0x82, (Side << 16u) | Side}, @@ -185,15 +190,15 @@ AgcDriver::QueueState Queue(const Block& block, std::uint32_t targetMask) { return queue; } -AgcDriver::Graphics::State TargetState(const Block& block, std::uint32_t exportedMask) { +AgcDriver::Graphics::State TargetState(const Block& block, std::uint32_t exportedMask, std::uint32_t samples) { const bool partial = exportedMask != 0xfu; - const auto decoded = AgcDriver::Graphics::DecodeState(Queue(block, exportedMask)); + const auto decoded = AgcDriver::Graphics::DecodeState(Queue(block, exportedMask, samples)); const auto& color = decoded.colors.front(); char detail[256]; - std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x of the 2-sample 4_4_4_4 target decoded %zu targets, %u samples, word mask 0x%04x, write mask 0x%x and %s shading", exportedMask, decoded.colors.size(), decoded.samples, color.partialWriteMask, decoded.blends.front().colorWriteMask, decoded.perSampleShading ? "per-sample" : "pixel-rate"); - Require(decoded.colors.size() == 1u && decoded.samples == Samples && color.samples == Samples && color.partialWriteMask == (partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); + std::snprintf(detail, sizeof(detail), "CB_TARGET_MASK 0x%x of the %u-sample 4_4_4_4 target decoded %zu targets, %u samples, word mask 0x%04x, write mask 0x%x and %s shading", exportedMask, samples, decoded.colors.size(), decoded.samples, color.partialWriteMask, decoded.blends.front().colorWriteMask, decoded.perSampleShading ? "per-sample" : "pixel-rate"); + Require(decoded.colors.size() == 1u && decoded.samples == samples && color.samples == samples && color.partialWriteMask == (partial ? AgcDriver::Graphics::PackedWriteMask(color.componentMapping, exportedMask) : 0u) && decoded.blends.size() == 1u && decoded.blends.front().colorWriteMask == (partial ? 0u : 0xfu) && decoded.blend.colorWriteMask == decoded.blends.front().colorWriteMask && decoded.perSampleShading == partial, detail); const auto merge = AgcDriver::Graphics::ExportTargetMerge(decoded); - Require(merge.mask == color.partialWriteMask && merge.samples == (partial ? Samples : 1u) && !merge.depthStencilWrites && merge.descriptor == (partial ? AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)) : std::array{}), "the decoded 2-sample target exported another merge than its own surface"); + Require(merge.mask == color.partialWriteMask && merge.samples == (partial ? samples : 1u) && !merge.depthStencilWrites && merge.descriptor == (partial ? AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)) : std::array{}), "the decoded " + std::to_string(samples) + "-sample target exported another merge than its own surface"); AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; state.colors = decoded.colors; @@ -258,7 +263,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& sta device.Draw(state, draw, shaders); } -std::vector LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture) { +std::vector LoadSamples(AgcDriver::VulkanDevice& device, const std::array& texture, std::uint32_t samples) { std::vector userData(16, 0u); const auto address = reinterpret_cast(Buffer.data()); const std::array buffer{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; @@ -274,13 +279,13 @@ std::vector LoadSamples(AgcDriver::VulkanDevice& device, const st }; request.useCache = false; const auto program = ShaderRecompiler::Recompile(request); - std::vector loaded(Side * Side * Samples); + std::vector loaded(Side * Side * samples); for (std::uint32_t first = 0; first < loaded.size(); first += Threads) { Buffer.fill(0xdeadbeefu); for (std::uint32_t tid = 0; tid < Threads; ++tid) { const auto index = first + tid; - const auto pixel = index / Samples; - const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % Samples}; + const auto pixel = index / samples; + const std::array words{0u, 0u, 0u, pixel % Side, pixel / Side, index % samples}; std::copy(words.begin(), words.end(), &Buffer[tid * Words]); } device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast(LoadCode.data())); @@ -306,6 +311,90 @@ double Inside(const std::array, 3>& triangle, double x, dou return std::min({EdgeDistance(triangle, x, y, 0), EdgeDistance(triangle, x, y, 1), EdgeDistance(triangle, x, y, 2)}); } +void RunSamples(AgcDriver::VulkanDevice& device, const Block& block, std::uint32_t samples) { + const auto color = DecodeTarget(block, samples); + const auto full = TargetState(block, 0xfu, samples); + Draw(device, full, Covering, Float32Program(First)); + const auto merged = TargetState(block, 0x1u, samples); + Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); + Draw(device, merged, Cutting, Float32Program(Second)); + Draw(device, full, Overlapping, Float32Program(Third)); + const auto texture = AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)); + const auto loaded = LoadSamples(device, texture, samples); + const auto background = Packed(First); + const auto written = (background & 0x0fffu) | (Packed(Second) & 0xf000u); + const auto over = Packed(Third); + Require(written != background && over != written && over != background, "the test colors do not differ in the merged nibble"); + const auto locations = AgcDriver::Graphics::SampleLocations(merged); + std::vector pixelsByCoverage(samples + 1u); + std::uint32_t checked = 0; + std::uint32_t overdrawn = 0; + std::uint32_t mergedAfterOverdraw = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + std::uint32_t covered = 0; + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto word = loaded[(y * Side + x) * samples + sample]; + char where[160]; + std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) holds 0x%04x", sample, x, y, word); + Require(word == written || word == background || word == over, std::string(where) + ", neither the merged word, the first draw's word nor the last draw's word"); + covered += word == written ? 1u : 0u; + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto distance = Inside(Cutting, px, py); + const auto overDistance = Inside(Overlapping, px, py); + if (std::fabs(distance) < EdgeMargin || std::fabs(overDistance) < EdgeMargin) continue; + ++checked; + const auto expected = overDistance > 0 ? over : distance > 0 ? written : background; + overdrawn += overDistance > 0 && distance > 0 ? 1u : 0u; + mergedAfterOverdraw += overDistance <= 0 && distance > 0 ? 1u : 0u; + Require(word == expected, std::string(where) + (overDistance > 0 ? ", which the last full write covers" : distance > 0 ? ", which only the merging triangle covers" : ", which no later triangle covers")); + } + ++pixelsByCoverage[covered]; + } + } + const auto partlyMerged = std::accumulate(pixelsByCoverage.begin() + 1, pixelsByCoverage.end() - 1, 0u); + Require(checked >= Side * Side * samples * 8u / 10u, "fewer than eight in ten samples are clear of the triangles' edges"); + Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[samples] != 0 && partlyMerged != 0, "the merged samples do not leave pixels unmerged, merged and partly merged"); + Require(overdrawn != 0 && mergedAfterOverdraw != 0, "the last full write does not cover some merged samples and leave others"); + Draw(device, full, Covering, Float32Program(First)); + const auto lowMerged = TargetState(block, 0x8u, samples); + Require(lowMerged.colors.front().partialWriteMask == 0x000fu, "a W write of the 4_4_4_4 target did not merge bits 3..0"); + Draw(device, merged, Cutting, Float32Program(Second)); + Draw(device, lowMerged, Crossing, Float32Program(Fourth)); + const auto twice = LoadSamples(device, texture, samples); + std::uint32_t checkedTwice = 0; + std::uint32_t mergedTwice = 0; + for (std::uint32_t y = 0; y < Side; ++y) { + for (std::uint32_t x = 0; x < Side; ++x) { + for (std::uint32_t sample = 0; sample < samples; ++sample) { + const auto px = x + static_cast(locations[sample].x); + const auto py = y + static_cast(locations[sample].y); + const auto high = Inside(Cutting, px, py); + const auto low = Inside(Crossing, px, py); + if (std::fabs(high) < EdgeMargin || std::fabs(low) < EdgeMargin) continue; + ++checkedTwice; + mergedTwice += high > 0 && low > 0 ? 1u : 0u; + auto expected = background; + if (high > 0) expected = (expected & 0x0fffu) | (Packed(Second) & 0xf000u); + if (low > 0) expected = (expected & 0xfff0u) | (Packed(Fourth) & 0x000fu); + const auto word = twice[(y * Side + x) * samples + sample]; + char where[192]; + std::snprintf(where, sizeof(where), "after an X merge and a W merge drawn back to back, sample %u of pixel (%u, %u) holds 0x%04x, expected 0x%04x", sample, x, y, word, expected); + Require(word == expected, where); + } + } + } + Require(checkedTwice >= Side * Side * samples * 8u / 10u && mergedTwice != 0, "the back-to-back merges leave fewer than eight in ten samples clear of their edges, or no sample inside both"); + std::printf("4_4_4_4 unorm %u-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, a full write drawn right after covered %u merged samples, and %u samples kept both of two merges drawn back to back\n", samples, partlyMerged, overdrawn, mergedTwice); +} + +bool SupportsSamples(const AgcDriver::Graphics::Context& context, std::uint32_t samples) { + VkImageFormatProperties properties{}; + const VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_STORAGE_BIT; + return (context.limits.framebufferColorSampleCounts & samples) != 0u && context.imageFormatProperties(context.physical, VK_FORMAT_R16_UINT, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &properties) == VK_SUCCESS && (properties.sampleCounts & samples) != 0u; +} + } int main() { @@ -318,81 +407,18 @@ int main() { std::printf("skipped, the device lacks %s\n", feature); return VulkanTestSkipped; } - Block block(4u * BlockBytes); - const auto color = DecodeTarget(block); - const auto full = TargetState(block, 0xfu); - Draw(*device, full, Covering, Float32Program(First)); - const auto merged = TargetState(block, 0x1u); - Require(merged.colors.front().partialWriteMask == 0xf000u, "an X write of the 4_4_4_4 target did not merge bits 15..12"); - Draw(*device, merged, Cutting, Float32Program(Second)); - Draw(*device, full, Overlapping, Float32Program(Third)); - const auto texture = AgcDriver::Graphics::EncodeTextureResource(AgcDriver::Graphics::TargetMergeSurface(color)); - const auto loaded = LoadSamples(*device, texture); - const auto background = Packed(First); - const auto written = (background & 0x0fffu) | (Packed(Second) & 0xf000u); - const auto over = Packed(Third); - Require(written != background && over != written && over != background, "the test colors do not differ in the merged nibble"); - const auto locations = AgcDriver::Graphics::SampleLocations(merged); - std::array pixelsByCoverage{}; - std::uint32_t checked = 0; - std::uint32_t overdrawn = 0; - std::uint32_t mergedAfterOverdraw = 0; - for (std::uint32_t y = 0; y < Side; ++y) { - for (std::uint32_t x = 0; x < Side; ++x) { - std::uint32_t covered = 0; - for (std::uint32_t sample = 0; sample < Samples; ++sample) { - const auto word = loaded[(y * Side + x) * Samples + sample]; - char where[160]; - std::snprintf(where, sizeof(where), "sample %u of pixel (%u, %u) holds 0x%04x", sample, x, y, word); - Require(word == written || word == background || word == over, std::string(where) + ", neither the merged word, the first draw's word nor the last draw's word"); - covered += word == written ? 1u : 0u; - const auto px = x + static_cast(locations[sample].x); - const auto py = y + static_cast(locations[sample].y); - const auto distance = Inside(Cutting, px, py); - const auto overDistance = Inside(Overlapping, px, py); - if (std::fabs(distance) < EdgeMargin || std::fabs(overDistance) < EdgeMargin) continue; - ++checked; - const auto expected = overDistance > 0 ? over : distance > 0 ? written : background; - overdrawn += overDistance > 0 && distance > 0 ? 1u : 0u; - mergedAfterOverdraw += overDistance <= 0 && distance > 0 ? 1u : 0u; - Require(word == expected, std::string(where) + (overDistance > 0 ? ", which the last full write covers" : distance > 0 ? ", which only the merging triangle covers" : ", which no later triangle covers")); - } - ++pixelsByCoverage[covered]; + std::vector> blocks; + std::uint32_t tested = 0; + for (const auto samples : SampleCounts) { + if (!SupportsSamples(context, samples)) { + std::printf("%u-sample R16_UINT storage color targets skipped, the device lacks them\n", samples); + continue; } + blocks.push_back(std::make_unique(4u * BlockBytes)); + RunSamples(*device, *blocks.back(), samples); + ++tested; } - Require(checked >= Side * Side * Samples * 8u / 10u, "fewer than eight in ten samples are clear of the triangles' edges"); - Require(pixelsByCoverage[0] != 0 && pixelsByCoverage[Samples] != 0 && pixelsByCoverage[1] != 0, "the merged samples do not leave pixels unmerged, merged and partly merged"); - Require(overdrawn != 0 && mergedAfterOverdraw != 0, "the last full write does not cover some merged samples and leave others"); - Draw(*device, full, Covering, Float32Program(First)); - const auto lowMerged = TargetState(block, 0x8u); - Require(lowMerged.colors.front().partialWriteMask == 0x000fu, "a W write of the 4_4_4_4 target did not merge bits 3..0"); - Draw(*device, merged, Cutting, Float32Program(Second)); - Draw(*device, lowMerged, Crossing, Float32Program(Fourth)); - const auto twice = LoadSamples(*device, texture); - std::uint32_t checkedTwice = 0; - std::uint32_t mergedTwice = 0; - for (std::uint32_t y = 0; y < Side; ++y) { - for (std::uint32_t x = 0; x < Side; ++x) { - for (std::uint32_t sample = 0; sample < Samples; ++sample) { - const auto px = x + static_cast(locations[sample].x); - const auto py = y + static_cast(locations[sample].y); - const auto high = Inside(Cutting, px, py); - const auto low = Inside(Crossing, px, py); - if (std::fabs(high) < EdgeMargin || std::fabs(low) < EdgeMargin) continue; - ++checkedTwice; - mergedTwice += high > 0 && low > 0 ? 1u : 0u; - auto expected = background; - if (high > 0) expected = (expected & 0x0fffu) | (Packed(Second) & 0xf000u); - if (low > 0) expected = (expected & 0xfff0u) | (Packed(Fourth) & 0x000fu); - const auto word = twice[(y * Side + x) * Samples + sample]; - char where[192]; - std::snprintf(where, sizeof(where), "after an X merge and a W merge drawn back to back, sample %u of pixel (%u, %u) holds 0x%04x, expected 0x%04x", sample, x, y, word, expected); - Require(word == expected, where); - } - } - } - Require(checkedTwice >= Side * Side * Samples * 8u / 10u && mergedTwice != 0, "the back-to-back merges leave fewer than eight in ten samples clear of their edges, or no sample inside both"); - std::printf("4_4_4_4 unorm 2-sample partial write tests passed: %u partly merged pixels kept bits 15..12 merged into their covered samples only, a full write drawn right after covered %u merged samples, and %u samples kept both of two merges drawn back to back\n", pixelsByCoverage[1], overdrawn, mergedTwice); + if (tested == 0) return VulkanTestSkipped; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index e52b45acc2..00b4d24034 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -163,7 +163,7 @@ - [4_4_4_4 partial writes, sample positions](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a multisampled 4_4_4_4 UNORM target runs the pixel shader once per sample. For a shader the guest runs per pixel it throws on inputs that would follow the invocation's sample: the ancillary word and, unless every sample sits at the pixel centre, centroid, sample-located or pull-model barycentrics, Z, 1/W and positions at the centroid or a sample; off-centre samples make centre barycentrics throw without interpolation functions and fragment shader barycentrics. Centre barycentrics and positions, upper-left and fixed-point positions and the front face are kept, so each invocation reaches the console's colour and kill decision. A shader that writes guest memory throws, as each covered sample would repeat the writes, also when the guest iterates it itself (the request does not say). Not measured on a PS5. - [4_4_4_4 CB resolves](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB resolve from a packed color target throws, so a 2-sample 4_4_4_4 target cannot be resolved by the color block. - [4_4_4_4 partial writes, merged coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a partial write of a one-sample 4_4_4_4 UNORM target drawn with merged coverage (a multisampled rasterizer over a one-sample target, as radeonsi line and polygon smoothing does) throws: the merge leaves the target out of the colour attachments, so such a draw would rasterize the extra samples with no one-sample attachment for `VK_NV_framebuffer_mixed_samples` coverage reduction, a combination that is not tested. -- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample and 2-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. +- [4_4_4_4 partial writes, measured hardware](../../../core/libs/prx/libSceAgcDriver/tests/execution/PackedUnorm4444MultisamplePartialWrite.cpp) (libSceAgcDriver) - the single-sample merges and the 2-, 4- and 8-sample merges are measured only on an NVIDIA RTX 4070 SUPER, not on other host GPUs or a PS5. - [COPY_DATA from the GPU clock](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 9) writes the host steady clock in 10 ns ticks when the packet runs, the same counter as the `RELEASE_MEM` timestamp, not the console's GPU clock. The `COPY_DATA` cache policy fields are accepted and have no effect. - [COPY_DATA from a register](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, `SRC_SEL` 0) copies the value the queue holds for a shader, context or user config register (`COUNT_SEL` two consecutive ones) and throws for a register with no value. A dispatch records `COMPUTE_DISPATCH_INITIATOR` and, for `DISPATCH_DIRECT`, `COMPUTE_DIM_X/Y/Z` in the shader registers; `DISPATCH_INDIRECT` clears the three counts. Neither the register values the CP writes nor the copy's ordering against in-flight dispatches have been measured on hardware. - [EVENT_WRITE stream-out statistics events](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x46) - `SAMPLE_STREAMOUTSTATS1`-`3` and `SAMPLE_STREAMOUTSTATS` (event types 0x01-0x03 and 0x20 in [navi10_enum.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/include/navi10_enum.h#L14155-L14188)) are accepted on the graphics queue as a 2-dword packet with event index 0 and do nothing. [nvd.h](https://github.com/torvalds/linux/blob/0fcb05b72344f40806cbe810de8872f1698bf053/drivers/gpu/drm/amd/amdgpu/nvd.h#L326-L347) documents these events only with the sampling index (3, or `EVENT_INDEX__SAMPLE_STREAMOUTSTATS*` 8-11 on gfx10); the index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured on hardware. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. The sampling indices throw that sampling to memory is not implemented, and other indices, other sizes and the compute queue throw. [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address builds the index-0 packet: nothing is written and nothing throws. Whether the console's builder emits a sampling packet for an address is not known. From abc07a1192c0bed708fc71a4e1f68f309dbffb2b Mon Sep 17 00:00:00 2001 From: priyanshrv-netizen <275132450+priyanshrv-netizen@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:09:52 +0530 Subject: [PATCH 066/318] feat(libc): implement _Vacopy Copy the 24-byte System V argument list, writing the overflow pointer before the register offsets as the console's libc.prx does. Co-Authored-By: Claude Opus 5 (1M context) --- core/libs/prx/libc/src/RuntimeSupport.cpp | 9 +++++++ core/libs/tests/GuestStdioVariadic.cpp | 31 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/core/libs/prx/libc/src/RuntimeSupport.cpp b/core/libs/prx/libc/src/RuntimeSupport.cpp index 02058dc4af..54d3ed0104 100644 --- a/core/libs/prx/libc/src/RuntimeSupport.cpp +++ b/core/libs/prx/libc/src/RuntimeSupport.cpp @@ -95,6 +95,15 @@ void APS5_VABI cxa_finalize_nid_postfix(void* dsoHandle) { CxaFinalize_nid_no_patch(dsoHandle); } +void APS5_VABI _Vacopy_nid_postfix(unsigned char* destination, const unsigned char* source) { + std::uint64_t overflowArea; + std::memcpy(&overflowArea, source + 16, sizeof(overflowArea)); + std::memcpy(destination + 16, &overflowArea, sizeof(overflowArea)); + unsigned char offsets[16]; + std::memcpy(offsets, source, sizeof(offsets)); + std::memcpy(destination, offsets, sizeof(offsets)); +} + unsigned int APS5_VABI _Atomic_fetch_add_4_nid_postfix(volatile unsigned int* target, unsigned int value, int memoryOrder) { (void)memoryOrder; return GccAtomicFetchAdd(target, value); diff --git a/core/libs/tests/GuestStdioVariadic.cpp b/core/libs/tests/GuestStdioVariadic.cpp index 62ead8be7f..7bc4ba7cdb 100644 --- a/core/libs/tests/GuestStdioVariadic.cpp +++ b/core/libs/tests/GuestStdioVariadic.cpp @@ -16,6 +16,7 @@ int APS5_VABI vfwprintf_nid_postfix(FileStream*, const char16_t*, void*); int APS5_VABI fwprintf_nid_postfix(FileStream*, const char16_t*, ...); int APS5_VABI vswscanf_nid_postfix(const char16_t*, const char16_t*, void*); int APS5_VABI vswscanf_s_nid_postfix(const char16_t*, const char16_t*, void*); +void APS5_VABI _Vacopy_nid_postfix(void*, const void*); } #ifdef _WIN32 @@ -67,6 +68,36 @@ static int APS5_VABI ScanWideSized(const char16_t* input, const char16_t* format return result; } +static long APS5_VABI SumThroughCopy(int count, ...) { + APS5_VA_BEGIN(count); + unsigned char before[24]; + std::memcpy(before, &args, sizeof(before)); +#ifdef _WIN32 + __builtin_sysv_va_list copy; +#else + std::va_list copy; +#endif + _Vacopy_nid_postfix(©, &args); + Require(std::memcmp(©, &args, sizeof(before)) == 0, "_Vacopy copies all 24 bytes"); + long sum = 0; + for (int index = 0; index < count; ++index) sum += __builtin_va_arg(copy, long) * (index + 1); + Require(std::memcmp(&args, before, sizeof(before)) == 0, "_Vacopy leaves the source untouched"); + for (int index = 0; index < count; ++index) sum -= __builtin_va_arg(args, long); + APS5_VA_END(); + return sum; +} + +static void CheckVacopy() { + long expected = 0; + for (long index = 1; index <= 9; ++index) expected += index * 100 * index - index * 100; + Require(SumThroughCopy(9, 100L, 200L, 300L, 400L, 500L, 600L, 700L, 800L, 900L) == expected, "_Vacopy copy reads every argument"); + unsigned char overlap[32]; + for (int index = 0; index < 32; ++index) overlap[index] = static_cast(index); + _Vacopy_nid_postfix(overlap + 8, overlap); + const unsigned char shifted[32] = {0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; + Require(std::memcmp(overlap, shifted, sizeof(overlap)) == 0, "_Vacopy copies the overflow area before the offsets"); +} + static std::string Contents(FileStream& stream) { std::fflush(stream.GetHandle()); std::rewind(stream.GetHandle()); From 89cc8368df7e1ca5e42d9018ed713b02ef6f1da3 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:35:19 +0100 Subject: [PATCH 067/318] perf(agc): park freed snapshot buffers in a bounded spare pool --- .../Graphics/src/ShaderResources.cpp | 46 ++++++++++++++----- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 68b4c1b480..d0bb065ef3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -120,13 +120,9 @@ struct CachedTexture { // The untrackedKey the snapshot was last read or compared equal at, for a surface outside the // write watch; zero when none was taken. std::array untracked{}; - std::vector carried; - std::uint64_t carriedPresent = 0; std::uint64_t surfaceBytes = 0; }; -constexpr std::uint64_t CarriedBufferMaxPresents = 16; - // Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the // eviction takes the back. APS5_NO_TEXTURE_HASH=1 finds entries by scanning the list (the index is // still kept), the linear lookup of before. @@ -135,6 +131,8 @@ struct TextureCache { std::list entries; std::unordered_map::iterator, TextureKeyHash> index; std::unordered_multimap::iterator> views; + std::vector> spare; + std::uint64_t spareBytes = 0; std::uint64_t bytes = 0; std::size_t passed = 0; std::chrono::steady_clock::time_point passedSince{}; @@ -269,6 +267,33 @@ void touchTexture(TextureCache& cache, std::list::iterator it) { it->present = Recorder::Presents(); } +constexpr std::uint64_t SpareSnapshotBytes = 512ull << 20; + +void parkSnapshot(TextureCache& cache, std::vector& bytes) { + const auto size = bytes.size(); + if (size == 0) return; + while (!cache.spare.empty() && cache.spareBytes + size > SpareSnapshotBytes) { + cache.spareBytes -= cache.spare.front().size(); + cache.spare.erase(cache.spare.begin()); + } + if (cache.spareBytes + size <= SpareSnapshotBytes) { + cache.spareBytes += size; + cache.spare.push_back(std::move(bytes)); + } + bytes = std::vector{}; +} + +std::vector spareSnapshot(TextureCache& cache, std::size_t bytes) { + for (auto spare = cache.spare.begin(); spare != cache.spare.end(); ++spare) { + if (spare->size() != bytes) continue; + cache.spareBytes -= bytes; + std::vector taken = std::move(*spare); + cache.spare.erase(spare); + return taken; + } + return {}; +} + bool admitTexture(TextureCache& cache, const TextureKey& key, std::uint64_t incoming, std::uint64_t budget) { if (cache.bytes + incoming <= budget) return true; if (const auto now = std::chrono::steady_clock::now(); now - cache.passedSince >= std::chrono::seconds(1)) { @@ -857,7 +882,6 @@ std::shared_ptr cachedTexture(const Context& context, std::span recycled; if (auto it = findTexture(cache, key); it != cache.entries.end()) { if (it->source != nullptr) { - if (!it->carried.empty() && Recorder::Presents() - it->carriedPresent > CarriedBufferMaxPresents) it->carried = std::vector{}; // The view follows the storage image, whatever the GPU wrote to it since; guest memory // written meanwhile is taken in by refreshing the image. A fast clear the image cannot // see (keys, and no pending results to prefer) ends the view: a snapshot holds the clear. @@ -903,15 +927,13 @@ std::shared_ptr cachedTexture(const Context& context, std::spanbytes.size() == bytes) recycled = std::move(it->bytes); - else if (it->carried.size() == bytes && Recorder::Presents() - it->carriedPresent <= CarriedBufferMaxPresents) recycled = std::move(it->carried); + else parkSnapshot(cache, it->bytes); eraseTexture(cache, it); ++cache.counts.stale; } + if (source != nullptr) parkSnapshot(cache, recycled); + else if (recycled.empty()) recycled = spareSnapshot(cache, bytes); CachedTexture entry{key, address, source != nullptr ? std::vector{} : (recycled.empty() ? std::vector(bytes) : std::move(recycled)), nullptr, *keys, snapshotGeneration}; - if (source != nullptr) { - entry.carried = std::move(recycled); - entry.carriedPresent = Recorder::Presents(); - } entry.committed = std::move(committed); entry.surfaceBytes = bytes; if (source != nullptr) { @@ -949,7 +971,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, *context.detiler, resource, components, entry.bytes, depthCompare); entry.accounted = entry.texture->AllocationBytes(); constexpr std::size_t SnapshotCompareBytes = 256u << 10u; - if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) std::vector().swap(entry.bytes); + if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) parkSnapshot(cache, entry.bytes); counters.snapshots.fetch_add(1, std::memory_order_relaxed); } const auto budget = sampledBudget(context, cache); @@ -1301,6 +1323,8 @@ void ClearCachedTextures(VkDevice device) { if (it->key.device == device) eraseTexture(sampled, it++); else ++it; } + sampled.spare.clear(); + sampled.spareBytes = 0; } auto& storage = StorageTextures(); std::vector> cleared; From eab9b23a5271b2ccecef1e257065c8e315831861 Mon Sep 17 00:00:00 2001 From: javierUDP Date: Tue, 20 Oct 2026 14:54:02 -0300 Subject: [PATCH 068/318] test(relinker): execute the split 256-bit store The stub that splits an unaligned 256-bit store is checked through its bytes but never run, so nothing confirms that the two halves land where the original store would, including across a page boundary, which is the case the split exists for. The x86-64 execution section now runs the stub at three operand offsets and checks the 32 bytes it writes and the pages around them. --- .../codegen/tests/Amd64OnlyConverterTests.cpp | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 3e77c91db6..f4191551c5 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -1903,6 +1903,40 @@ void mcommitExecution() { } munmap(code, 4096); } + +void wideStoreExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0xC5, 0xFC, 0x11, 0x00}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "256-bit store stub was not produced"); + auto body = match->StubBody; + body[match->ReturnBranchOffset] = 0xC3; + void* code = mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + require(code != MAP_FAILED, "cannot map executable memory for the stub"); + std::memcpy(code, body.data(), body.size()); + alignas(32) std::uint8_t source[32]; + for (std::size_t index = 0; index < sizeof(source); ++index) source[index] = static_cast(index + 1); + auto* pages = static_cast(mmap(nullptr, 8192, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + require(pages != MAP_FAILED, "cannot map the destination pages"); + for (const std::size_t offset : {std::size_t{0}, std::size_t{1}, std::size_t{4080}}) { + std::memset(pages, 0, 8192); + auto* destination = pages + offset; + asm volatile( + "vmovups (%[source]), %%ymm0\n\t" + "sub $128, %%rsp\n\t" + "call *%[code]\n\t" + "add $128, %%rsp\n\t" + : + : [source] "r"(source), [code] "r"(code), "a"(destination) + : "memory", "ymm0"); + require(std::memcmp(destination, source, sizeof(source)) == 0, "256-bit store stub did not write both halves"); + require(std::all_of(pages, destination, [](const std::uint8_t value) { return value == 0; }), "256-bit store stub wrote before its operand"); + require(std::all_of(destination + sizeof(source), pages + 8192, [](const std::uint8_t value) { return value == 0; }), + "256-bit store stub wrote past its operand"); + } + munmap(pages, 8192); + munmap(code, 4096); +} #else void reciprocalExecution() {} void registerFormExecution() {} @@ -1947,6 +1981,7 @@ void rdtscpExecution() { } #else void rdtscpExecution() {} +void wideStoreExecution() {} #endif void scannerZeroTail() { @@ -2005,6 +2040,7 @@ int main() { rosettaLegacyScalarCompares(); converterRosettaLegacyScalarCompare(); rdtscpExecution(); + wideStoreExecution(); wideStoreSplit(); converterWideStoreWindows(); converterJumpTableTargets(); From 4c150894766a0f8db1e6e65c0c862b5443f33724 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Wed, 21 Oct 2026 07:34:01 +1300 Subject: [PATCH 069/318] style: address review of the scratch address operand change Format tests/FlatScratch.cpp, drop the doubled blank line before its CMake block and move the technical debt entry into the Functional list. Co-authored-by: Prophet069 <291514246+Prophet069@users.noreply.github.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 - .../prx/libSceAgcDriver/tests/FlatScratch.cpp | 146 +++++++++++++----- docs/dev/debt/shader.md | 5 +- 3 files changed, 109 insertions(+), 43 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 825078f002..d52937934a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3202,7 +3202,6 @@ if(BUILD_TESTING) 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) diff --git a/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp b/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp index 4db6cb7c5b..73aef4bdf0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/FlatScratch.cpp @@ -1,48 +1,118 @@ -#include "Translation/TranslationContext.hpp" #include "RdnaDecoder/RdnaMemoryOpDecoder.hpp" +#include "Translation/TranslationContext.hpp" #include #include #include 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 code{(0x37u<<26)|(opcode<<18)|(seg<<14)|offset,(2u<<24)|(saddr<<16)|addr}; - return DecodeRdnaFlat(0x154cu,code,0); +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 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; +static void Reject(unsigned seg, unsigned saddr, unsigned addr) { + try { + (void)Decode(seg, saddr, addr); + } catch (const std::runtime_error &) { + ++tests; + return; } - 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; + throw std::runtime_error("invalid FLAT accepted"); } -int main(){ +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 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 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); + const std::array 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 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); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index b4cfb47d7f..8257fb1be5 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -165,13 +165,10 @@ - [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. - [BDA anchor halfword reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) retain volatile byte loads even for noncoherent memory. On RX 6650 XT / RADV Mesa 26.2.4, the valid SPIR-V for two adjacent nonvolatile byte loads returned `0x80a0` for bytes `0xa0, 0xa1`, where lookup-based reads returned `0xa1a0`. Adding the volatile operand makes the same GPU exactness fixture pass, including mixed direct/fallback lanes. The driver transformation responsible has not been isolated; removing this restriction needs equivalent readback coverage on the affected driver. Single-byte reads keep their original access mask. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. +- [GFX10 scratch address operands](../../../core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp) (shader recompiler) - `core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp` and `core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp` use the SV/SS/ST scratch operand widths described by LLVM FLATInstructions.td at 3eb867f9b63ae2881bbe3c25d3c12842f2bdc55e (operand pseudos L557-L630, GFX10 encoding L2973-L3038). Decoding and IR tests cover the one-dword VGPR offset; PS5 hardware address-result validation remains outstanding. Unsupported scalar registers still throw. - [SDWA source selects on `v_cvt_f32_ubyte1`, `v_cvt_f32_ubyte2` and `v_cvt_f32_ubyte3`](../../../core/shader/recompiler/RdnaDecoder/src/RdnaVectorOpDecoder.cpp) (shader recompiler) - byte N is taken from source 0 after `SRC0_SEL` and `SRC0_SEXT` select and extend it, since SDWA supplies source 0 (RDNA 2 ISA guide 6.8 and 13.3.7) and the opcode reads `S0.u[8N+7:8N]` (12.8). Not measured on hardware. - [Flag structurization](../../../core/shader/recompiler/ControlFlow/src/Structurizer.cpp) (shader recompiler) - a graph that every structurizer attempt refuses, irreducible and non-terminating graphs included, is rebuilt with one reach flag per block and one again flag per loop (`structurizeWithFlags`): each block keeps one copy and runs behind a guard on its flag, and each strongly connected component becomes a loop whose single latch repeats it while a back edge set its flag. The output has about 8 times the input's blocks and one route variable per block, and ends in an empty Return that never runs. Only blocks that start the top-level order unconditionally run unguarded, so resource planning also sees the static paths that skip a guarded block and can plan extra descriptor candidates for them. The attempts still run first. Shared merge splitting refuses an attempt once the graph has grown past 16 times its block count at the start of splitting (at least 64 blocks); succeeding attempts grew at most 10.6 times on random graphs of up to 128 blocks, and an attempt that would succeed only beyond the budget leaves the graph to a later attempt or to this construction. Splitting that adds one block per round stays within that budget until its 4 rounds per block run out, so the attempts can still take more than 10 s (9 of 2,000 random graphs of up to 128 blocks, 63 s at most). `APS5_FORCE_STRUCTURIZER_FALLBACK=1` builds every graph this way, for tests - [Scalar buffer descriptors](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) use only the base address, stride and record count for resources accessed exclusively by scalar loads; their fourth word is normalized to zero, following AMD RDNA2 ISA section 7.2.2. Vector and mixed accesses retain descriptor validation. Native Vulkan readbacks cover ignored fourth-word fields on Windows/NVIDIA; their console behavior has not been measured. - [Spanned wave64 compute](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp) (shader recompiler) - `APS5_SPANNED_WAVE=1`, off by default, runs a single-wave wave64 compute program at one lane per invocation on a 32-lane host subgroup and exchanges ballots, shuffles and reductions between the two host subgroups through workgroup memory and barriers. The driver turns it on only where compute subgroups can be pinned at 32 full lanes (`subgroupSizeControl` and `computeFullSubgroups`), passes it as `SpirvTarget::spannedWaves` (part of the cache key and the serialized request), and creates the pipelines of spanned programs with `REQUIRE_FULL_SUBGROUPS`, since the lane number `SubgroupId * 32 + SubgroupLocalInvocationId` needs two full subgroups. Those barriers are correct only where every invocation of the workgroup reaches them, which holds for the guest's wave-uniform control flow. The emulated 64-bit LDS atomics ([LockedLdsUpdate](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp)) run only on the lanes the exec mask keeps, so their lock loop ballots within its own host subgroup, as its elect already does. Any other cross-lane exchange emitted inside per-lane control flow would hang a spanned wave in the same way, and the emitter does not reject one: there is no uniformity check. The only check so far is manual: disassemble the module (`spirv-dis`), list the selection and loop constructs that enclose each `OpControlBarrier`, and read the condition of each enclosing selection. In Ghost of Yōtei's program 0x80003af600 (PPSA26344), 108 of 111 barriers sat only in loops on wave-uniform conditions and 3 inside per-invocation conditionals, all in the lock loop. Other programs and titles have not been scanned. An earlier, cheaper ballot (one barrier instead of three) also lost the device for a reason that is not understood, which may be another barrier placement problem. Measured on an RTX 4090 (Windows): an offline replay of that program and two Yōtei runs that previously lost the device (NVIDIA TDR) now run past it, and [LdsMaskWaterfall.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsMaskWaterfall.cpp) runs both modes and covers a half-inactive wave. Stray (PPSA02101) on Intel ANV and a Demon's Souls (PPSA01342) request replay showed no fault, before this fix - [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. AMD's proprietary driver does not get `robustBufferAccess2` ([RobustBufferAccess.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp)), so its loads keep their selections. On an RX 6700 XT with driver 32.0.21045.5002, enabling the feature broke `agc_driver_layered_volume_draw` and `agc_driver_layered_volume_vertex`: the vertex draw left the volume untouched even with the selections kept. `agc_driver_subvector_loop` failed too, with the selected loads: the driver compiled the begin instruction's `exec_lo == 0` as a test of the whole 64-bit mask, so the high half of a wave64 skipped its loop. The begin instruction now tests for an empty mask as `(exec_lo | exec_hi) == 0` rather than `exec_lo == 0 && exec_hi == 0`, and that driver compiles it correctly. The cause of the layered-volume failures was not found. - [Merged hull programs](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) (registered binary type 3) run whole in the hull stage, one invocation per output control point. On the console hull lane i keeps local lane i's registers, which with fewer input than output control points stop matching the patch's control points from i = inputs on; here invocation i holds control point min(i, inputs - 1) of its own patch. A wave-mask branch (`s_cbranch_exec[n]z`, `s_cbranch_vcc[n]z`) tests the invocation's own lane, so it can skip a block, scalar instructions included, that the console runs for the wave; `v_readfirstlane_b32` returns the invocation's own value, not the first active lane's, and another lane's register throws. The register layout (`s3`, `v0`-`v5`, as for the separate halves) is not measured on hardware. s[0:1] hold the hull user-data address (SH 0x102/0x103) when a draw sets it and zero otherwise, so a program reading it unset faults at the null address. - -### GFX10 scratch address operands - -`core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp` and `core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp` use the SV/SS/ST scratch operand widths described by LLVM FLATInstructions.td at 3eb867f9b63ae2881bbe3c25d3c12842f2bdc55e (operand pseudos L557-L630, GFX10 encoding L2973-L3038). Decoding and IR tests cover the one-dword VGPR offset; PS5 hardware address-result validation remains outstanding. Unsupported scalar registers still throw. From 1c9a522bef304eff3ec51d21b881101c411c6a38 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 20 Oct 2026 10:07:59 -0400 Subject: [PATCH 070/318] fix(shader): decide resource branch conditions from their uniform operand Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 67 +++++++++++++++++++ .../Optimization/SrtWalker/SrtEvaluator.hpp | 1 + .../Optimization/SrtWalker/SrtTape.hpp | 2 +- .../Optimization/src/ResourceMaterializer.cpp | 2 +- .../src/SrtWalker/SrtEvaluator.cpp | 47 +++++++++++-- .../Optimization/src/SrtWalker/SrtTape.cpp | 26 ++++++- 6 files changed, 134 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 09ada8fddd..1d73fa90d0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -944,6 +944,72 @@ void verifyPureFlatSlots() { require(Detail::ComputePureFlatSlots(plan) == Pure{0, 0, 0, 0}, "pure flat slots: an incomplete plan was classified"); } +void verifyLaneMaskGuardedDescriptorSources() { + using namespace ShaderRecompiler; + constexpr std::uint32_t Format8888UNorm = 56; + constexpr std::uint32_t Type2D = 9; + constexpr std::uint32_t Zero = 0x00000000u; + constexpr std::uint32_t NegativeZero = 0x80000000u; + constexpr std::uint32_t One = 0x3f800000u; + struct alignas(256) Texture { std::array bytes{}; }; + static Texture texture; + const auto base = static_cast(reinterpret_cast(texture.bytes.data())); + const std::array valid{static_cast(base >> 8u), static_cast((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + const std::array invalid{0x43700000u, 0x43070000u, 0x3b888889u, 0x3bf2b9d6u, 0x3b088889u, 0x3b72b9d6u, 0x3f7f7777u, 0x3f7f0d46u}; + const std::array sampler{0u, 0u, 0u, 0u}; + std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array outputV{static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu}; + std::array srt{}; + const auto fill = [&](const std::array& image, std::uint32_t mode) { + std::copy(image.begin(), image.end(), srt.begin()); + std::copy(sampler.begin(), sampler.end(), srt.begin() + 8); + std::copy(outputV.begin(), outputV.end(), srt.begin() + 16); + srt[20] = mode; + }; + const auto srtAddress = static_cast(reinterpret_cast(srt.data())); + const std::array userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::array capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + RecompileRequest request{}; + const std::vector code{0xf4000400u, 0xfa000050u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbf8cc07fu, 0xd40d006au, 0x00002080u, 0xbf860006u, 0xf40c0200u, 0xfa000000u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u}; + request.shader = {ShaderStage::Compute, 0x24000u, code, 0, {}}; + request.context.waveSize = 32; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.bdaAbiVersion = BdaAbi::Version; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + + fill(invalid, Zero); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.samplers.size() == 1u, "lane-mask guarded sources: the image and its sampler were not planned"); + require(!plan->controlFlow.empty() && std::ranges::any_of(plan->controlFlow, [](const ResourceBlock& block) { return block.condition != nullptr; }), "lane-mask guarded sources: the VCC branch around the image kept no condition"); + for (const auto mode : {Zero, NegativeZero}) { + fill(invalid, mode); + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::ranges::all_of(capture->snapshot.images[0].dwords, [](std::uint32_t word) { return word == 0u; }), "lane-mask guarded sources: the T# behind a branch no lane takes was not left null"); + } + fill(invalid, One); + { + AgcDriver::ShaderMemory memory({}); + expectFailure([&] { static_cast(memory.Capture(request)); }, "runtime image descriptor is invalid", "lane-mask guarded sources: an invalid T# behind a branch the lanes may take was accepted"); + } + fill(valid, One); + { + AgcDriver::ShaderMemory memory({}); + const auto capture = memory.Capture(request); + require(capture->snapshot.images.size() == 1u && std::equal(valid.begin(), valid.end(), capture->snapshot.images[0].dwords.begin()), "lane-mask guarded sources: the T# behind a branch the lanes may take was not read"); + } +} + void verifyGuardedDescriptorSources() { using namespace ShaderRecompiler; constexpr std::uint32_t Format8888UNorm = 56; @@ -7444,6 +7510,7 @@ int main(int argc, char** argv) { verifyMayEvaluate(); verifyPureFlatSlots(); verifyGuardedDescriptorSources(); + verifyLaneMaskGuardedDescriptorSources(); verifySrtTape(); verifyImageTableKeys(); verifyImageTables(); diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index d666acd0f8..2fa129cb49 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -173,6 +173,7 @@ public: BasicEvaluator& operator=(const BasicEvaluator&) = delete; static bool MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask = nullptr); + static bool MayDecide(const IrResourcePlan& program, IrValue* value); bool Evaluate(TNode value, std::uint32_t& result); bool EvaluateWide(TNode value, std::uint64_t& result); void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; } diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp index 0dcb1a168d..2e833ba3e7 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtTape.hpp @@ -26,7 +26,7 @@ private: static std::shared_ptr Compile(const IrResourcePlan& plan); enum class Op : std::uint8_t { Fail, Const, UserData, ShaderBase, Read, ReadConstBuffer, ExtractHalf, AddCarry, ConstructU64, - ConvertF32U32, ConvertU32F32, FPMul32, FPTrunc32, FPIsNan32, FPOrdLessThanEqual32, FPOrdGreaterThanEqual32, + ConvertF32U32, ConvertU32F32, FPMul32, FPTrunc32, FPIsNan32, FPOrdLessThanEqual32, FPOrdGreaterThanEqual32, FPUnordLessThanEqual32, FPUnordGreaterThanEqual32, IAdd32, IAdd64, ISub32, ISub64, IMul32, IMul64, SMin32, UMin32, BitwiseAnd32, BitwiseAnd64, BitwiseOr32, BitwiseXor32, BitwiseNot32, ShiftLeftLogical32, ShiftLeftLogical64, ShiftRightLogical32, ShiftRightLogical64, ShiftRightArithmetic32, ShiftRightArithmetic64, BitFieldUExtract, BitFieldSExtract, BitFieldInsert, Select, IEqual32, INotEqual32, SLessThan32, ULessThan32, UGreaterThan32, diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3a9a072113..afd94b4e75 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -2281,7 +2281,7 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const PrepareImageModes(plan.info); plan.uniformFill = source.uniformFill; for (auto& block : plan.controlFlow) { - if (block.condition != nullptr && !Detail::Evaluator::MayEvaluate(source, block.condition)) block.condition = nullptr; + if (block.condition != nullptr && !Detail::Evaluator::MayDecide(source, block.condition)) block.condition = nullptr; } ownPlanValues(plan); const auto addSource = [&plan](std::uint32_t index) { diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index 3369a7457a..1412970d44 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -289,6 +289,8 @@ std::optional evaluatedArguments(IrOpcode opcode) { case IrOpcode::FPMul32: case IrOpcode::FPOrdLessThanEqual32: case IrOpcode::FPOrdGreaterThanEqual32: + case IrOpcode::FPUnordLessThanEqual32: + case IrOpcode::FPUnordGreaterThanEqual32: case IrOpcode::BitwiseAnd32: case IrOpcode::BitwiseAnd64: case IrOpcode::BitwiseOr32: @@ -447,6 +449,20 @@ bool BasicEvaluator::MayEvaluate(const IrResourcePlan& program, IrValue* return known.at(value->Resolve()); } +template +bool BasicEvaluator::MayDecide(const IrResourcePlan& program, IrValue* value) { + if (value == nullptr) return false; + if (MayEvaluate(program, value)) return true; + value = value->Resolve(); + const auto decides = [&program, value](std::size_t index) { return value->Argument(index) != nullptr && MayDecide(program, value->Argument(index)); }; + switch (value->Opcode()) { + case IrOpcode::LogicalNot: return value->ArgumentCount() == 1u && decides(0); + case IrOpcode::LogicalAnd: + case IrOpcode::LogicalOr: return value->ArgumentCount() == 2u && (decides(0) || decides(1)); + default: return false; + } +} + template bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& result) { std::uint64_t a = 0; @@ -599,6 +615,18 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul return true; } return false; + case IrOpcode::FPUnordLessThanEqual32: + if (binary()) { + result = !(Float32(a) > Float32(b)) ? 1u : 0u; + return true; + } + return false; + case IrOpcode::FPUnordGreaterThanEqual32: + if (binary()) { + result = !(Float32(a) < Float32(b)) ? 1u : 0u; + return true; + } + return false; case IrOpcode::BitwiseAnd32: if (binary()) { result = static_cast(a & b); @@ -774,17 +802,22 @@ bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& resul } return false; case IrOpcode::LogicalAnd: - if (binary()) { - result = (a != 0u) && (b != 0u) ? 1u : 0u; + case IrOpcode::LogicalOr: { + const bool decisive = inst.Opcode() == IrOpcode::LogicalOr; + const bool first = Arg(inst, 0, a); + if (first && (a != 0u) == decisive) { + result = decisive ? 1u : 0u; return true; } - return false; - case IrOpcode::LogicalOr: - if (binary()) { - result = (a != 0u) || (b != 0u) ? 1u : 0u; + const bool second = Arg(inst, 1, b); + if (second && (b != 0u) == decisive) { + result = decisive ? 1u : 0u; return true; } - return false; + if (!first || !second) return false; + result = decisive ? 0u : 1u; + return true; + } case IrOpcode::LogicalXor: if (binary()) { result = (a != 0u) != (b != 0u) ? 1u : 0u; diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp index 681bec7693..13b3cad246 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtTape.cpp @@ -245,6 +245,8 @@ private: case IrOpcode::FPIsNan32: return operation(Op::FPIsNan32, value, context, depth, 1); case IrOpcode::FPOrdLessThanEqual32: return operation(Op::FPOrdLessThanEqual32, value, context, depth, 2); case IrOpcode::FPOrdGreaterThanEqual32: return operation(Op::FPOrdGreaterThanEqual32, value, context, depth, 2); + case IrOpcode::FPUnordLessThanEqual32: return operation(Op::FPUnordLessThanEqual32, value, context, depth, 2); + case IrOpcode::FPUnordGreaterThanEqual32: return operation(Op::FPUnordGreaterThanEqual32, value, context, depth, 2); case IrOpcode::BitwiseAnd32: return operation(Op::BitwiseAnd32, value, context, depth, 2); case IrOpcode::BitwiseAnd64: return operation(Op::BitwiseAnd64, value, context, depth, 2); case IrOpcode::BitwiseOr32: return operation(Op::BitwiseOr32, value, context, depth, 2); @@ -343,6 +345,24 @@ bool SrtTape::evaluate(Frame& frame, std::uint32_t index, std::uint64_t& result) } bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) const { + if (node.op == Op::LogicalAnd || node.op == Op::LogicalOr) { + const bool decisive = node.op == Op::LogicalOr; + std::uint64_t first = 0; + std::uint64_t second = 0; + const bool firstEvaluated = evaluate(frame, node.arguments[0], first); + if (firstEvaluated && (first != 0u) == decisive) { + result = decisive ? 1u : 0u; + return true; + } + const bool secondEvaluated = evaluate(frame, node.arguments[1], second); + if (secondEvaluated && (second != 0u) == decisive) { + result = decisive ? 1u : 0u; + return true; + } + if (!firstEvaluated || !secondEvaluated) return false; + result = decisive ? 0u : 1u; + return true; + } std::array operand{}; std::int32_t origin = -1; for (std::uint8_t index = 0; index < node.argumentCount; ++index) { @@ -426,6 +446,8 @@ bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) con case Op::FPIsNan32: result = std::isnan(float32(a)) ? 1u : 0u; return true; case Op::FPOrdLessThanEqual32: result = float32(a) <= float32(b) ? 1u : 0u; return true; case Op::FPOrdGreaterThanEqual32: result = float32(a) >= float32(b) ? 1u : 0u; return true; + case Op::FPUnordLessThanEqual32: result = !(float32(a) > float32(b)) ? 1u : 0u; return true; + case Op::FPUnordGreaterThanEqual32: result = !(float32(a) < float32(b)) ? 1u : 0u; return true; case Op::IAdd32: result = static_cast(a + b); return true; case Op::IAdd64: result = a + b; return true; case Op::ISub32: result = static_cast(a - b); return true; @@ -485,8 +507,8 @@ bool SrtTape::compute(Frame& frame, const Node& node, std::uint64_t& result) con case Op::SLessThan32: result = std::bit_cast(static_cast(a)) < std::bit_cast(static_cast(b)) ? 1u : 0u; return true; case Op::ULessThan32: result = static_cast(a) < static_cast(b) ? 1u : 0u; return true; case Op::UGreaterThan32: result = static_cast(a) > static_cast(b) ? 1u : 0u; return true; - case Op::LogicalAnd: result = (a != 0u) && (b != 0u) ? 1u : 0u; return true; - case Op::LogicalOr: result = (a != 0u) || (b != 0u) ? 1u : 0u; return true; + case Op::LogicalAnd: + case Op::LogicalOr: return false; case Op::LogicalXor: result = (a != 0u) != (b != 0u) ? 1u : 0u; return true; case Op::LogicalNot: result = a == 0u ? 1u : 0u; return true; } From f956527f6739d47dd483f65b1a6eb73e8bd8dea6 Mon Sep 17 00:00:00 2001 From: maximaxi01 Date: Mon, 19 Oct 2026 18:32:57 -0400 Subject: [PATCH 071/318] fix(libSceFont): use current style for glyph and kerning scales --- .../prx/libSceFont/include/FontInternal.hpp | 4 +- core/libs/prx/libSceFont/src/Layout.cpp | 4 +- core/libs/prx/libSceFont/src/Open.cpp | 2 - core/libs/prx/libSceFont/src/Render.cpp | 20 ++++-- core/libs/prx/libSceFont/src/Style.cpp | 9 +-- core/libs/tests/GuestFont.cpp | 69 +++++++++++++++++++ 6 files changed, 87 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index 8c68010f10..d6dfa76547 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -34,8 +34,6 @@ struct FontState { std::shared_ptr> faceData; FT_Face face = nullptr; std::vector fallbacks; - float scaleW = 16.0f; - float scaleH = 16.0f; std::map scriptLanguages; std::map typographicFeatures; int attribute = 0; @@ -134,7 +132,7 @@ void CachedStyleSetScalar(CachedStyle& style, float value); void ClearRenderOutputs(FontGlyphMetrics* metrics, FontRenderOutput* result); int ComputeHorizontalLayout(FontHandle handle, const StyleStateBlock* style, std::uint8_t* outWords); int ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, std::uint8_t* outWords); -int GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle); +int GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle, float* usedScaleW = nullptr, float* usedScaleH = nullptr); int RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRenderSurface* surface, float x, float y, FontGlyphMetrics* metrics, FontRenderOutput* result); } diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index a59a3b5f2f..4755cdbe9d 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -365,7 +365,7 @@ int Font::ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, return SCE_FONT_OK; } -int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle) { +int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMetrics* metrics, bool useCachedStyle, float* usedScaleW, float* usedScaleH) { const auto clearMetrics = [&] { if (metrics) *metrics = {}; }; @@ -434,6 +434,8 @@ int Font::GetCharGlyphMetrics(FontHandle handle, std::uint32_t code, FontGlyphMe metrics->Vertical.bearingX = static_cast(vertBearingX) / 64.0f * ratioX; metrics->Vertical.bearingY = static_cast(vertBearingY) / 64.0f * ratioY; metrics->Vertical.advance = static_cast(slot->metrics.vertAdvance) / 64.0f * ratioY; + if (usedScaleW) *usedScaleW = scaleW; + if (usedScaleH) *usedScaleH = scaleH; return finish(SCE_FONT_OK); } diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index 21e0272990..0c44ecbb9e 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -397,8 +397,6 @@ int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFon ReleaseFontLock(source, sourceLock); FontState& targetState = ResetState(target); if (FontState* sourceState = TryGetState(fontHandle)) { - targetState.scaleW = sourceState->scaleW; - targetState.scaleH = sourceState->scaleH; if (sourceState->faceData && !sourceState->faceData->empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); for (const FallbackFace& fallback : sourceState->fallbacks) AddStateFallbackFace(targetState, fallback.data, fallback.subFontIndex); } diff --git a/core/libs/prx/libSceFont/src/Render.cpp b/core/libs/prx/libSceFont/src/Render.cpp index 3f78fa635a..eb661bbf40 100644 --- a/core/libs/prx/libSceFont/src/Render.cpp +++ b/core/libs/prx/libSceFont/src/Render.cpp @@ -188,7 +188,12 @@ int APS5_VABI sceFontGetKerning(FontHandle fontHandle, std::uint32_t preCode, st if (!font || font->magic != HANDLE_MAGIC || !AcquireFontLock(font, fontLock)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; *kerning = {}; const FontState* state = TryGetState(fontHandle); - if (state) LoadKerning(*state, state->scaleW, state->scaleH, preCode, code, kerning); + if (state) { + float scaleW = 0.0f; + float scaleH = 0.0f; + StyleStateGetScalePixel(&font->style, &scaleW, &scaleH); + LoadKerning(*state, scaleW, scaleH, preCode, code, kerning); + } ReleaseFontLock(font, fontLock); return SCE_FONT_OK; } @@ -373,8 +378,7 @@ int APS5_VABI sceFontGenerateCharGlyph(FontHandle fontHandle, std::uint32_t code *pGlyph = nullptr; if (!fontHandle) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; if (code == 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; - const FontState* state = TryGetState(fontHandle); - if (!state) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (!TryGetState(fontHandle)) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; const std::uint8_t glyphForm = detail ? detail->glyph_form : 0; const std::uint8_t metricsForm = detail ? detail->metrics_form : 0; const std::uint16_t formOptions = detail ? detail->form_options : 0; @@ -385,7 +389,9 @@ int APS5_VABI sceFontGenerateCharGlyph(FontHandle fontHandle, std::uint32_t code if ((glyphForm != 0 && metricsForm == 0) || glyphForm > 1 || metricsForm > 4) return SCE_FONT_ERROR_INVALID_PARAMETER; if (glyphMemory && glyphMemory->mem_kind != MEMORY_MAGIC) return SCE_FONT_ERROR_INVALID_PARAMETER; FontGlyphMetrics metrics{}; - const int rc = GetCharGlyphMetrics(fontHandle, code, &metrics, false); + float scaleW = 0.0f; + float scaleH = 0.0f; + const int rc = GetCharGlyphMetrics(fontHandle, code, &metrics, false, &scaleW, &scaleH); if (rc != SCE_FONT_OK) return rc; auto* generated = new (std::nothrow) GeneratedGlyph(); if (!generated) return SCE_FONT_ERROR_ALLOCATION_FAILED; @@ -395,13 +401,13 @@ int APS5_VABI sceFontGenerateCharGlyph(FontHandle fontHandle, std::uint32_t code generated->glyph.flags = formOptions; generated->glyph.glyph_form = glyphForm; generated->glyph.metrics_form = metricsForm; - generated->glyph.em_size = ClampToU16(state->scaleH); + generated->glyph.em_size = ClampToU16(scaleH); generated->glyph.baseline = ClampToU16(metrics.Horizontal.bearingY); generated->glyph.height_px = ClampToU16(metrics.height); generated->glyph.origin_x = 0; generated->glyph.origin_y = generated->glyph.baseline; - generated->glyph.scale_x = state->scaleW; - generated->glyph.base_scale = state->scaleH; + generated->glyph.scale_x = scaleW; + generated->glyph.base_scale = scaleH; generated->glyph.memory = glyphMemory; generated->owner = fontHandle; generated->outline.outline_flags = generated->glyph.flags; diff --git a/core/libs/prx/libSceFont/src/Style.cpp b/core/libs/prx/libSceFont/src/Style.cpp index e4adb89793..10e7602467 100644 --- a/core/libs/prx/libSceFont/src/Style.cpp +++ b/core/libs/prx/libSceFont/src/Style.cpp @@ -135,14 +135,7 @@ int APS5_VABI sceFontGetTypographicDesign(FontHandle fontHandle, int typographic } int APS5_VABI sceFontSetScalePixel(FontHandle fontHandle, float w, float h) { - return UpdateFontStyle(fontHandle, [&](FontHandleNative* font) { - const int changed = StyleStateSetScalePixel(&font->style, w, h); - if (FontState* state = TryGetState(fontHandle)) { - state->scaleW = w; - state->scaleH = h; - } - return changed; - }); + return UpdateFontStyle(fontHandle, [&](FontHandleNative* font) { return StyleStateSetScalePixel(&font->style, w, h); }); } int APS5_VABI sceFontSetScalePoint(FontHandle fontHandle, float w, float h) { diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 80184223b8..ed0df7d9bc 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -1,6 +1,7 @@ #include "prx/libSceFont/include/FontDriver.hpp" #include "tests/FontFixture.hpp" #include +#include #include #include #include @@ -30,6 +31,7 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary, std::uint32_t, std::uint32_t, cons int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); int APS5_VABI sceFontOpenFontFile(FontLibrary, const char*, std::uint32_t, const FontOpenDetail*, FontHandle*); int APS5_VABI sceFontCloseFont(FontHandle); +int APS5_VABI sceFontOpenFontInstance(FontHandle, FontHandle, FontHandle*); int APS5_VABI sceFontSetScriptLanguage(FontHandle, int, int); int APS5_VABI sceFontGetScriptLanguage(FontHandle, int, int*); int APS5_VABI sceFontSetTypographicDesign(FontHandle, int, int); @@ -39,6 +41,9 @@ int APS5_VABI sceFontGetAttribute(FontHandle, int, int*); int APS5_VABI sceFontSetResolutionDpi(FontHandle, std::uint32_t, std::uint32_t); int APS5_VABI sceFontGetResolutionDpi(FontHandle, std::uint32_t*, std::uint32_t*); int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); +int APS5_VABI sceFontSetScalePoint(FontHandle, float, float); +int APS5_VABI sceFontGetScalePixel(FontHandle, float*, float*); +int APS5_VABI sceFontGlyphGetScalePixel(FontGlyph, float*, float*); int APS5_VABI sceFontBindRenderer(FontHandle, FontRenderer); int APS5_VABI sceFontUnbindRenderer(FontHandle); int APS5_VABI sceFontSetupRenderScalePixel(FontHandle, float, float); @@ -198,6 +203,68 @@ static bool KerningIs(const FontKerning& kerning, float offsetX) { return kerning.offsetX == offsetX && kerning.offsetY == 0.0f && kerning.positionX == 0.0f && kerning.positionY == 0.0f; } +static void CheckGlyphScale(FontHandle font, const FontMemory* memory, float expectedW, float expectedH) { + float width = 0.0f; + float height = 0.0f; + Require(sceFontGetScalePixel(font, &width, &height) == SCE_FONT_OK); + Require(width == expectedW && height == expectedH); + FontGlyph glyph = nullptr; + Require(sceFontGenerateCharGlyph(font, 'A', nullptr, &glyph) == SCE_FONT_OK && glyph != nullptr); + Require(sceFontGlyphGetScalePixel(glyph, &width, &height) == SCE_FONT_OK); + if (width != expectedW || height != expectedH) { + std::fprintf(stderr, "Generated glyph scale %.3f x %.3f differs from font scale %.3f x %.3f\n", width, height, expectedW, expectedH); + } + Require(width == expectedW && height == expectedH); + Require(glyph->em_size == static_cast(std::lround(expectedH))); + Require(sceFontDeleteGlyph(memory, &glyph) == SCE_FONT_OK && glyph == nullptr); +} + +static void CheckPointAndDpiScale(FontHandle font, FontLibrary library, const FontMemory* memory, FontRenderer renderer) { + FontKerning kerning{}; + Require(sceFontSetScalePoint(font, 20.0f, 30.0f) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 40.0f, 60.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -8.0f)); + Require(sceFontSetResolutionDpi(font, 216, 288) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 60.0f, 120.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -12.0f)); + Require(sceFontSetScalePoint(font, 50.0f, 40.0f) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 150.0f, 160.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -30.0f)); + Require(sceFontSetResolutionDpi(font, 0, 0) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 50.0f, 40.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -10.0f)); + Require(sceFontSetScalePixel(font, 100.0f, 75.0f) == SCE_FONT_OK); + Require(sceFontSetResolutionDpi(font, 144, 216) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 100.0f, 75.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -20.0f)); + Require(sceFontSetScalePoint(font, 20.0f, 30.0f) == SCE_FONT_OK); + CheckGlyphScale(font, memory, 40.0f, 90.0f); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -8.0f)); + FontHandle instance = nullptr; + Require(sceFontOpenFontInstance(font, nullptr, &instance) == SCE_FONT_OK && instance != nullptr); + CheckGlyphScale(instance, memory, 40.0f, 90.0f); + Require(sceFontGetKerning(instance, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -8.0f)); + Require(sceFontSetScalePoint(instance, 30.0f, 40.0f) == SCE_FONT_OK); + CheckGlyphScale(instance, memory, 60.0f, 120.0f); + CheckGlyphScale(font, memory, 40.0f, 90.0f); + Require(sceFontCloseFont(instance) == SCE_FONT_OK); + Require(sceFontBindRenderer(font, renderer) == SCE_FONT_OK); + Require(sceFontSetupRenderScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); + Require(sceFontGetRenderScaledKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -20.0f)); + Require(sceFontGetKerning(font, 'A', 'V', &kerning) == SCE_FONT_OK && KerningIs(kerning, -8.0f)); + Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); + const std::vector data = KerningFont(); + FontHandle fresh = nullptr; + Require(sceFontOpenFontMemory(library, data.data(), static_cast(data.size()), nullptr, &fresh) == SCE_FONT_OK); + float width = 0.0f; + float height = 0.0f; + Require(sceFontGetScalePixel(fresh, &width, &height) == SCE_FONT_OK); + CheckGlyphScale(fresh, memory, width, height); + Require(sceFontCloseFont(fresh) == SCE_FONT_OK); + Require(sceFontSetResolutionDpi(font, 144, 144) == SCE_FONT_OK); + Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); +} + int main() { constexpr std::uint32_t SystemFontSet = 0x18070043u; const std::filesystem::path fontRoot = std::filesystem::temp_directory_path() / ("anyps5_guest_font-" + std::to_string(std::random_device{}())); @@ -421,6 +488,8 @@ int main() { Require(sceFontUnbindRenderer(font) == SCE_FONT_OK); Require(sceFontGetRenderScaledKerning(font, 'A', 'V', &kerning) == SCE_FONT_ERROR_NOT_BOUND_RENDERER); + CheckPointAndDpiScale(font, library, &memory, renderer); + ParsedText parsed{"AV", 0, font}; FontTextSource source{}; Require(sceFontTextSourceInit(&source, parsed.text, 2, ParseText, &parsed) == SCE_FONT_OK); From 5702e1f9386a82889ce5783bf37854f06fefab31 Mon Sep 17 00:00:00 2001 From: javierUDP Date: Tue, 20 Oct 2026 18:25:33 -0300 Subject: [PATCH 072/318] test(relinker): include algorithm for the converter test std::all_of The new checks call std::all_of, which the file reached only through another header. --- core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index f4191551c5..de30a25dc5 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include From 34e3fbf47c68063079bc6db573cd5cba6fcacbee Mon Sep 17 00:00:00 2001 From: Seif Allah Karoui <21332330+SeifKaroui@users.noreply.github.com> Date: Tue, 20 Oct 2026 17:30:51 +0100 Subject: [PATCH 073/318] fix(shader): remove unobserved phi cycles before resource planning Preserve phi graphs with transitive non-phi users and remove unused cycles before building the SRT plan. Extend the IR regression with dead and live cycles, an empty program, and repeated elimination. AI-assisted: OpenAI Codex --- .../prx/libSceAgcDriver/tests/ConstantPhi.cpp | 78 +++++++++++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + .../Optimization/DeadCodeEliminator.hpp | 1 + .../src/UnobservedPhiEliminator.cpp | 42 ++++++++++ core/shader/recompiler/Recompiler.cpp | 2 + docs/dev/debt/shader.md | 2 + 6 files changed, 126 insertions(+) create mode 100644 core/shader/recompiler/Optimization/src/UnobservedPhiEliminator.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp index 2cc4a8b370..09ef9b6dac 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ConstantPhi.cpp @@ -79,6 +79,83 @@ void CheckDiamond(unsigned mode) { } } +void CheckUnobservedPhiCycles() { + IrProgram empty; + DeadCodeEliminator{}.EliminateUnobservedPhis(empty); + IrProgram program; + for (std::uint32_t id = 0; id < 4u; ++id) { + auto& block = program.CreateBlock(); + program.BlockOrder().push_back(&block); + BlockInfo info; + info.id = id; + info.terminator.kind = TerminatorKind::Return; + program.Metadata().blockInfo.push_back(info); + } + auto& entry = *program.BlockOrder()[0]; + auto& header = *program.BlockOrder()[1]; + auto& latch = *program.BlockOrder()[2]; + auto& exit = *program.BlockOrder()[3]; + program.SetEntryBlock(entry); + entry.AddBranch(&header); + header.AddBranch(&latch); + header.AddBranch(&exit); + latch.AddBranch(&header); + program.Metadata().blockInfo[0].terminator.kind = TerminatorKind::Branch; + program.Metadata().blockInfo[0].terminator.trueBlock = 1u; + auto& headerInfo = program.Metadata().blockInfo[1]; + headerInfo.terminator.kind = TerminatorKind::ConditionalBranch; + headerInfo.terminator.trueBlock = 2u; + headerInfo.terminator.falseBlock = 3u; + program.Metadata().blockInfo[2].terminator.kind = TerminatorKind::Branch; + program.Metadata().blockInfo[2].terminator.trueBlock = 1u; + IrBuilder builder(program); + builder.SetInsertionPoint(entry); + auto& zero = builder.Constant(0u); + auto& producer = builder.Emit(IrOpcode::LaneId, IrType::U32, {}); + const auto phi = [&](IrBlock& block, IrType type = IrType::U32) -> IrValue& { + auto& value = program.CreateValue(IrOpcode::Phi, type); + block.AppendInstruction(&value); + return value; + }; + auto& first = phi(header); + auto& second = phi(latch); + auto& tail = phi(header); + auto& self = phi(header); + auto& liveFirst = phi(header); + auto& liveSecond = phi(latch); + auto& condition = phi(header, IrType::Bool); + first.AddPhiOperand(&entry, &producer); + first.AddPhiOperand(&latch, &second); + second.AddPhiOperand(&header, &first); + tail.AddPhiOperand(&entry, &zero); + tail.AddPhiOperand(&latch, &second); + self.AddPhiOperand(&entry, &zero); + self.AddPhiOperand(&latch, &self); + liveFirst.AddPhiOperand(&entry, &zero); + liveFirst.AddPhiOperand(&latch, &liveSecond); + liveSecond.AddPhiOperand(&header, &liveFirst); + condition.AddPhiOperand(&entry, &builder.ConstantBool(true)); + condition.AddPhiOperand(&latch, &condition); + headerInfo.condition = &condition; + builder.SetInsertionPoint(header); + static_cast(builder.Emit(IrOpcode::Reference, IrType::Void, {&condition})); + builder.SetInsertionPoint(latch); + auto& store = builder.Emit(IrOpcode::StoreVectorWindow, IrType::Void, {&zero, &liveSecond}); + ValidateProgram(program, true); + DeadCodeEliminator{}.EliminateUnobservedPhis(program); + ValidateProgram(program, true); + Require(first.Parent() == nullptr && second.Parent() == nullptr && tail.Parent() == nullptr && self.Parent() == nullptr, "phi cycles: an unobserved cycle or its tail survived"); + Require(!first.HasUses() && !second.HasUses() && !tail.HasUses() && !self.HasUses(), "phi cycles: a removed cycle retained uses"); + Require(liveFirst.Parent() == &header && liveSecond.Parent() == &latch && liveFirst.Argument(1) == &liveSecond && liveSecond.Argument(0) == &liveFirst, "phi cycles: an observed cycle was changed"); + Require(store.Parent() == &latch && store.Argument(1) == &liveSecond, "phi cycles: an observable store was changed"); + Require(condition.Parent() == &header && headerInfo.condition == &condition && condition.Argument(1) == &condition, "phi cycles: a branch condition was changed"); + DeadCodeEliminator{}.Eliminate(program); + Require(producer.Parent() == nullptr && !producer.HasUses(), "phi cycles: ordinary DCE retained a released producer"); + DeadCodeEliminator{}.EliminateUnobservedPhis(program); + ValidateProgram(program, true); + Require(entry.Instructions().empty() && header.Instructions().size() == 3u && latch.Instructions().size() == 2u, "phi cycles: repeated elimination changed the live graph"); +} + void CheckLongAncillaryForwarding() { IrProgram program; auto& block = program.CreateBlock(); @@ -127,6 +204,7 @@ int main() { try { for (unsigned mode = 0; mode < 5u; ++mode) CheckDiamond(mode); CheckLongAncillaryForwarding(); + CheckUnobservedPhiCycles(); std::puts("constant phi dominance tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index bb32aec198..445d8cf507 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -100,6 +100,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/SsaBuilder/SsaRegisterStateCleanup.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ConstantFolder.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/DeadCodeEliminator.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/UnobservedPhiEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/HostInterpolationChecker.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/ReadLaneEliminator.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Optimization/src/MaskedSelectEliminator.cpp diff --git a/core/shader/recompiler/Optimization/include/Optimization/DeadCodeEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/DeadCodeEliminator.hpp index 3aef0d5211..6e820d5fe3 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DeadCodeEliminator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DeadCodeEliminator.hpp @@ -10,6 +10,7 @@ namespace ShaderRecompiler { class DeadCodeEliminator { public: void Eliminate(IrProgram& program) const; + void EliminateUnobservedPhis(IrProgram& program) const; void Eliminate(std::span blocks) const; void RemoveIdentities(IrProgram& program) const; void RemoveIdentities(std::span blocks) const; diff --git a/core/shader/recompiler/Optimization/src/UnobservedPhiEliminator.cpp b/core/shader/recompiler/Optimization/src/UnobservedPhiEliminator.cpp new file mode 100644 index 0000000000..a4ef3bc3f8 --- /dev/null +++ b/core/shader/recompiler/Optimization/src/UnobservedPhiEliminator.cpp @@ -0,0 +1,42 @@ +#include "Optimization/DeadCodeEliminator.hpp" +#include +#include +#include + +namespace ShaderRecompiler { + +void DeadCodeEliminator::EliminateUnobservedPhis(IrProgram& program) const { + std::unordered_set phis; + for (IrBlock* block : program.BlockOrder()) { + for (IrValue* value : block->Instructions()) { + if (value->IsPhi()) phis.insert(value); + } + } + std::unordered_set observed; + std::vector pending; + for (IrValue* phi : phis) { + if (std::ranges::any_of(phi->Uses(), [&](IrValue* user) { return !phis.contains(user); })) { + observed.insert(phi); + pending.push_back(phi); + } + } + while (!pending.empty()) { + IrValue* value = pending.back(); + pending.pop_back(); + for (IrValue* argument : value->Arguments()) { + if (phis.contains(argument) && observed.insert(argument).second) pending.push_back(argument); + } + } + std::vector unobserved; + for (IrValue* phi : phis) { + if (observed.contains(phi)) continue; + unobserved.push_back(phi); + for (std::size_t index = 0; index < phi->ArgumentCount(); ++index) phi->ReplaceArgument(index, nullptr); + } + for (IrValue* phi : unobserved) { + phi->Parent()->RemoveInstruction(phi); + phi->Invalidate(); + } +} + +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 9246de6bda..7176a9414c 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -319,6 +319,8 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat tessellationLowering.Lower(program, inputInfo.vertex->tess, request.target.tessellation ? &*request.target.tessellation : nullptr); } + deadCodeEliminator.EliminateUnobservedPhis(program); + deadCodeEliminator.Eliminate(program); constexpr SrtWalker srtWalker; srtWalker.BuildPlan(program); deadCodeEliminator.Eliminate(program); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d2fe3155a5..8657d7b8bb 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -29,6 +29,8 @@ - [Unlinked calls](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) - the `s_swappc_b64` a table call's compare chain falls back to, when its target matches no linked copy, records the `UnlinkedCall` fault with the target and the program counter and stops the invocation, or continues after the call in programs whose faults do not stop invocations (see `bdaStopsInvocations`). The driver lists those calls in the request (`linkedCallFallbacks`), and it reports the fault like a BDA fault, naming the target: a dispatch that checks its faults when it completes throws, one whose check runs in a deferred write-back logs it and continues. The hardware would jump to the target, which is not modelled. Any other `s_swappc_b64` whose target is not resolved still throws when the shader is compiled. - [Cached source IR](../../../core/shader/recompiler/Recompiler.cpp) releases lazily allocated SSA construction tables after resource preparation and reclaims values unreachable from instructions, metadata and resource plans when retaining a pending source program. Immediate shader preparation skips reclamation because compilation consumes the program. Reclamation must account for every new IR pointer owner; a remaining external use throws. Live IDs are retained. Source and variant entries remain cached without an eviction bound. A pending source program is released after its first variant is obtained; memory beyond the measured title checkpoint and Linux performance remain unmeasured. + +- Cycles through non-phi instructions are not removed by [unobserved phi elimination](../../../core/shader/recompiler/Optimization/src/UnobservedPhiEliminator.cpp). The pass removes phi-only cycles before resource planning when no transitive user is a non-phi instruction. - [Vertex user-data tail jumps](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) link a linear raw vertex prolog's terminal `s_setpc_b64` through an unchanged user-data pair to its continuation. The instruction semantics follow AMD RDNA2 ISA section 12.3; the prolog convention follows Mesa `aco_select_vs_prolog.cpp` at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`. Console hardware has not been measured. Branching prologs, computed low pointers and continuations reading `s_getpc_b64` remain unsupported - [Raw fragment user-data tails](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) also link a terminal `s_setpc_b64` when the low half is unchanged user data and the last high-half writer is an unconditional immediate `s_mov_b32`. Other computed high halves remain unsupported. This follows AMD RDNA2 ISA section 12.3 and Mesa `aco_select_nir.cpp` (`create_fs_jump_to_epilog`), `aco_isel_helpers.cpp` (`convert_pointer_to_64_bit`) and `aco_lower_to_hw_instr.cpp` (`p_jump_to_epilog`) at `0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8`; console hardware has not been measured From 700be1869477a876cadc09bed94bac2239cfe209 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Mon, 19 Oct 2026 21:44:32 -0700 Subject: [PATCH 074/318] fix(tools): preserve weak guest module imports in audits --- tools/import_audit.py | 22 +++++++++++++--------- tools/tests/test_import_audit.py | 32 +++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 0e47d38e5d..121e3147f1 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -133,7 +133,7 @@ def elf_program_dynamic_symbols(path, data): if symbol_count is None: raise AuditError(f"{path}: dynamic symbol table has no supported hash table") symbol_offset = file_offset(symbol_address, symbol_count * symbol_size, "dynamic symbol table") - imports = set() + imports = {} exports = set() for index in range(symbol_count): name, info, other, section_index, _, _ = unpack("> 4 == 2 elif info >> 4 in (1, 2, 10) and other & 3 in (0, 3): exports.add(symbol) needed = {cstring(data, string_offset + value, string_offset + string_size, path, "needed library name") for value in needed_offsets} @@ -189,7 +189,7 @@ def elf_imports(path, data): imports, _, needed = elf_program_dynamic_symbols(path, data) return imports, needed sections = elf_sections(path, data) - symbols = set() + symbols = {} needed = set() for section in sections: kind, offset, size, link, entry = section[1], section[4], section[5], section[6], section[9] @@ -199,10 +199,11 @@ def elf_imports(path, data): strings_offset, strings_size = sections[link][4], sections[link][5] take(data, strings_offset, strings_size, path, "dynamic string table") for position in range(0, size, 24): - name, _, _, index, _, _ = unpack("> 4 == 2 elif kind == 6: if entry != 16 or size % 16 or link >= len(sections) or sections[link][1] != 3: raise AuditError(f"{path}: invalid dynamic section (entry size {entry}, size {size}, string table section {link})") @@ -448,7 +449,7 @@ def read_modules(directories): names = pe_exports(file, data) else: symbols, needed = elf_imports(file, data) - imports[file.name] = (sorted(symbols), sorted(needed)) + imports[file.name] = (sorted(symbols.items()), sorted(needed)) names = elf_exports(file, data) for name in names: exports[name.split(MODULE_SUFFIX, 1)[0]].add(file.name) @@ -574,9 +575,10 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu record["references"] += 1 record["weak"] = record["weak"] and weak for module, (symbols, needed) in (module_imports or {}).items(): - for nid in symbols: - record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0, "weak": False}) + for nid, weak in symbols: + record = records.setdefault((nid, None, module), {"nid": nid, "library": None, "module": module, "references": 0, "weak": weak}) record["references"] += 1 + record["weak"] = record["weak"] and weak for record in records.values(): nid, library = record["nid"], record["library"] providers = exports.get(nid, []) @@ -685,6 +687,8 @@ def label(record): else: mark = "" if record.get("name_verified", True) else " (name does not hash to this NID)" text = f"{record['nid']} {record['name']}{mark}" + if record["weak"]: + text += " (weak import)" return f"{text} (imported by {record['module']})" if record.get("module") else text @@ -692,7 +696,7 @@ def render(summary, records, missing, stops, missing_hosts=()): lines = [f"{summary['references']} references, {summary['unique_imports']} unique imports"] for kind in CLASSES: lines.append(f" {kind:<12}{summary['unique_by_class'][kind]:>6} imports {summary['references_by_class'][kind]:>6} references") - for title, kind in (("Absent: no built library exports these, so the loader fails", "absent"), ("Stub: exported but throws when called", "stub")): + for title, kind in (("Absent: no built library exports these", "absent"), ("Stub: exported but throws when called", "stub")): selected = [record for record in records if record["class"] == kind] if selected: lines.append("") diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 166dc4508f..c30c03e8ae 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -309,7 +309,7 @@ class ImportAuditTests(unittest.TestCase): records, _ = import_audit.audit(imports, exports, stubs, set(exports), set()) self.assertEqual(records[0]["class"], "stub") self.assertEqual(records[0]["providers"], ["literal.prx"]) - module_imports = {"guest.so": (["strlen"], [])} + module_imports = {"guest.so": ([("strlen", False)], [])} records, _ = import_audit.audit([], {"j4ViWNHEgww": ["libc.prx"]}, {}, {"libc.prx"}, set(module_imports), module_imports, {"strlen": ["other.so"]}) @@ -538,6 +538,28 @@ class ImportAuditTests(unittest.TestCase): self.assertIn("[library not named]", out) self.assertIn("EEEEEEEEEEE (imported by main.prx)", out) + def test_module_import_bindings_survive_the_audit(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + registry, result = work.root / "game.json", work.root / "audit.json" + write_registry(registry, []) + symbols = [("optional", 0, 0x22), ("required", 0, GLOBAL_FUNCTION), + ("mixed#first", 0, 0x22), ("mixed#second", 0, GLOBAL_FUNCTION)] + images = [build_elf(symbols), build_sectionless_elf(symbols), + build_sectionless_elf(symbols, gnu_hash=True)] + for index, image in enumerate(images): + with self.subTest(image=index): + (work.modules / "guest.so").write_bytes(image) + code, out, err = work.run(registry, "--json", str(result)) + self.assertEqual((code, err), (1, "")) + data = json.loads(result.read_text()) + records = {record["nid"]: record for record in data["imports"]} + self.assertTrue(records["optional"]["weak"]) + self.assertFalse(records["required"]["weak"]) + self.assertFalse(records["mixed"]["weak"]) + self.assertEqual(data["references"], 3) + self.assertIn("optional (weak import)", out) + def test_module_sidecars_do_not_change_the_audit(self): work = self.work (work.libs / "libSceA.prx").write_bytes(exporting("AAAAAAAAAAA")) @@ -776,7 +798,7 @@ class ImportAuditTests(unittest.TestCase): ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"]) expected_imports = import_audit.elf_imports(path, image) expected_exports = import_audit.elf_exports(path, image) - self.assertEqual(expected_imports, ({"AAAAAAAAAAA"}, {"libSceA.prx"})) + self.assertEqual(expected_imports, ({"AAAAAAAAAAA": False}, {"libSceA.prx"})) self.assertEqual(expected_exports, {"BBBBBBBBBBB"}) for section_offset in (len(image), len(image) + 64): for section_count in (1, 0): @@ -841,7 +863,7 @@ class ImportAuditTests(unittest.TestCase): ("AAAAAAAAAAA", 0, GLOBAL_FUNCTION), ("BBBBBBBBBBB", 1, GLOBAL_FUNCTION)], ["libSceA.prx"], gnu_hash=True) self.assertEqual(import_audit.elf_program_dynamic_symbols(Path("module.prx"), image), - ({"AAAAAAAAAAA"}, {"BBBBBBBBBBB"}, {"libSceA.prx"})) + ({"AAAAAAAAAAA": False}, {"BBBBBBBBBBB"}, {"libSceA.prx"})) def test_sectionless_hash_metadata_must_be_file_backed(self): for gnu_hash in (False, True): @@ -1298,8 +1320,8 @@ class ImportAuditTests(unittest.TestCase): def test_elf_imports_reader(self): image = build_elf([("one", 1, GLOBAL_FUNCTION), ("two", 0, GLOBAL_FUNCTION), ("three", 0, 0x22)], ["libc.prx", "libSceZip.prx"]) path = Path("mod.prx") - self.assertEqual(import_audit.elf_imports(path, image), ({"two", "three"}, {"libc.prx", "libSceZip.prx"})) - self.assertEqual(import_audit.elf_imports(path, exporting()), (set(), set())) + self.assertEqual(import_audit.elf_imports(path, image), ({"two": False, "three": True}, {"libc.prx", "libSceZip.prx"})) + self.assertEqual(import_audit.elf_imports(path, exporting()), ({}, set())) for cut in (10, 64, len(image) - 5): with self.assertRaisesRegex(import_audit.AuditError, "mod.prx"): import_audit.elf_imports(path, image[:cut]) From db3c47c87a4d6fae308dd5cdf647dc7e5a053423 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 18:55:11 -0300 Subject: [PATCH 075/318] fix(libc): follow the default heap's alignment, errno and realloc to zero as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- .../libs/prx/libc/include/ApplicationHeap.hpp | 1 + core/libs/prx/libc/src/ApplicationHeap.cpp | 18 +++- core/libs/prx/libc/src/FileAndHeap.cpp | 71 +++++++++++--- core/libs/prx/libc/src/GuestHeap.cpp | 17 ++-- core/libs/tests/ApplicationHeap.cpp | 6 +- core/libs/tests/GuestAllocatorFailures.cpp | 96 ++++++++++++++----- core/libs/tests/GuestHeapRounding.cpp | 8 +- core/libs/tests/GuestReallocalign.cpp | 47 +++++++-- docs/dev/debt/libc.md | 4 +- 9 files changed, 207 insertions(+), 61 deletions(-) diff --git a/core/libs/prx/libc/include/ApplicationHeap.hpp b/core/libs/prx/libc/include/ApplicationHeap.hpp index 00033a1d59..bd604dd95a 100644 --- a/core/libs/prx/libc/include/ApplicationHeap.hpp +++ b/core/libs/prx/libc/include/ApplicationHeap.hpp @@ -9,6 +9,7 @@ const void* ApplicationProcessParameters_nid_no_patch(); bool ApplicationGuestHeapApi_nid_no_patch(void** api); void ApplicationHeapInitialize_nid_no_patch(const void* processParameters); void ApplicationHeapRegister_nid_no_patch(void* const* api); +bool ApplicationHeapDefault_nid_no_patch(); void* ApplicationHeapAllocate_nid_no_patch(std::size_t bytes); void ApplicationHeapFree_nid_no_patch(void* pointer); void* ApplicationHeapReallocate_nid_no_patch(void* pointer, std::size_t bytes); diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 84a8f46eb7..5fc17a5651 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -2,6 +2,7 @@ #include "prx/libc/include/GuestHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include #include #include #include @@ -27,6 +28,7 @@ using UsableSize = std::size_t (APS5_VABI *)(void*); std::mutex heapMutex; std::array heapApi{}; +std::atomic heapKind{0}; std::once_flag heapInitialization; std::exception_ptr heapFailure; Initialize heapFinalize = nullptr; @@ -145,6 +147,18 @@ void ApplicationHeapRegister_nid_no_patch(void* const* api) { if (heapFinalized) throw std::runtime_error("application heap: allocator has been finalized"); if (heapApi[0] != nullptr && heapApi != replacement) throw std::runtime_error("application heap: cannot replace an active allocator"); heapApi = replacement; + heapKind.store(replacement == defaultApi() ? 1 : 2, std::memory_order_release); +} + +bool ApplicationHeapDefault_nid_no_patch() { + int kind = heapKind.load(std::memory_order_acquire); + if (kind == 0) { + std::unique_lock lock(heapMutex); + ensureInitialized(lock); + if (heapFailure) std::rethrow_exception(heapFailure); + kind = heapKind.load(std::memory_order_acquire); + } + return kind == 1; } void ApplicationHeapInitialize_nid_no_patch(const void* processParameters) { @@ -192,7 +206,7 @@ void ApplicationHeapFree_nid_no_patch(void* pointer) { } void* ApplicationHeapReallocate_nid_no_patch(void* pointer, std::size_t bytes) { - if (bytes == 0 && pointer != nullptr) { + if (bytes == 0 && pointer != nullptr && !ApplicationHeapDefault_nid_no_patch()) { ApplicationHeapFree_nid_no_patch(pointer); return nullptr; } @@ -227,7 +241,7 @@ extern "C" void APS5_VABI scriptingFreeMem_nid_postfix(void* pointer) { } void* ApplicationHeapRealign_nid_no_patch(void* pointer, std::size_t bytes, std::size_t alignment) { - if (bytes == 0) { + if (bytes == 0 && !ApplicationHeapDefault_nid_no_patch()) { ApplicationHeapFree_nid_no_patch(pointer); return nullptr; } diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 4b8f5328ba..d1349613d5 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -252,13 +253,13 @@ static void SortElements(unsigned char* base, size_t count, size_t size, Compare namespace { template -void* allocationOrNull(TAllocate allocate) { +void* allocationOrNull(TAllocate allocate, int error = 12) { try { return allocate(); } catch (const std::bad_alloc&) { } catch (const std::length_error&) { } - errno = 12; + errno = error; return nullptr; } @@ -266,6 +267,32 @@ bool validAlignment(std::size_t alignment) { return alignment != 0 && (alignment & (alignment - 1)) == 0; } +constexpr std::size_t DefaultHeapAlignment = 32; +constexpr std::size_t DefaultHeapAlignmentLimit = std::size_t{1} << 63; +constexpr std::size_t DefaultHeapOverhead = 0x1008f; +constexpr std::size_t DefaultHeapAlignedOverhead = 0x100af; + +std::size_t defaultHeapAlignment(std::size_t alignment) { + return std::bit_ceil(std::max(alignment, DefaultHeapAlignment)); +} + +bool defaultHeapRequestOverflows(std::size_t bytes, std::size_t alignment) { + const std::size_t overhead = alignment <= DefaultHeapAlignment ? DefaultHeapOverhead : defaultHeapAlignment(alignment) + DefaultHeapAlignedOverhead; + return bytes > std::numeric_limits::max() - overhead; +} + +template +void* defaultHeapAllocation(std::size_t bytes, std::size_t alignment, TAllocate allocate) { + if (defaultHeapRequestOverflows(bytes, alignment)) return nullptr; + return allocationOrNull(allocate, 22); +} + +template +void* heapAllocation(std::size_t bytes, TAllocate allocate) { + if (!ApplicationHeapDefault_nid_no_patch()) return allocationOrNull(allocate); + return defaultHeapAllocation(bytes, 0, allocate); +} + } extern "C" { @@ -546,7 +573,7 @@ size_t APS5_VABI malloc_usable_size_nid_postfix(void* pointer) { } void* APS5_VABI malloc_nid_postfix(size_t size) { - return allocationOrNull([size] { return ApplicationHeapAllocate_nid_no_patch(size); }); + return heapAllocation(size, [size] { return ApplicationHeapAllocate_nid_no_patch(size); }); } void APS5_VABI free_nid_postfix(void* ptr) { @@ -554,7 +581,7 @@ void APS5_VABI free_nid_postfix(void* ptr) { } void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { - return allocationOrNull([ptr, newSize] { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); }); + return heapAllocation(newSize, [ptr, newSize] { return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); }); } void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { @@ -569,34 +596,50 @@ void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { } void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { - if (!validAlignment(alignment)) { - errno = 22; - return nullptr; + if (!ApplicationHeapDefault_nid_no_patch()) { + if (!validAlignment(alignment)) { + errno = 22; + return nullptr; + } + return allocationOrNull([alignment, size] { return ApplicationHeapAlign_nid_no_patch(alignment, size); }); } - return allocationOrNull([alignment, size] { return ApplicationHeapAlign_nid_no_patch(alignment, size); }); + if (alignment >= DefaultHeapAlignmentLimit) return nullptr; + const std::size_t rounded = defaultHeapAlignment(alignment); + return defaultHeapAllocation(size, alignment, [rounded, size] { return ApplicationHeapAlign_nid_no_patch(rounded, size); }); } void* APS5_VABI aligned_alloc_nid_postfix(size_t alignment, size_t size) { + if (ApplicationHeapDefault_nid_no_patch() && !validAlignment(alignment)) return nullptr; return memalign_nid_postfix(alignment, size); } void* APS5_VABI reallocalign_nid_postfix(void* ptr, size_t size, size_t alignment) { - if (size != 0 && !validAlignment(alignment)) { - errno = 22; - return nullptr; + if (!ApplicationHeapDefault_nid_no_patch()) { + if (size != 0 && !validAlignment(alignment)) { + errno = 22; + return nullptr; + } + return allocationOrNull([ptr, size, alignment] { return ApplicationHeapRealign_nid_no_patch(ptr, size, alignment); }); } - return allocationOrNull([ptr, size, alignment] { return ApplicationHeapRealign_nid_no_patch(ptr, size, alignment); }); + if (alignment >= DefaultHeapAlignmentLimit) return nullptr; + const std::size_t rounded = defaultHeapAlignment(alignment); + return defaultHeapAllocation(size, alignment, [ptr, size, rounded] { return ApplicationHeapRealign_nid_no_patch(ptr, size, rounded); }); } void* APS5_VABI calloc_nid_postfix(size_t count, size_t size) { - return allocationOrNull([count, size] { return ApplicationHeapCalloc_nid_no_patch(count, size); }); + const auto allocate = [count, size] { return ApplicationHeapCalloc_nid_no_patch(count, size); }; + if (!ApplicationHeapDefault_nid_no_patch()) return allocationOrNull(allocate); + if (size != 0 && count > std::numeric_limits::max() / size) return nullptr; + return defaultHeapAllocation(count * size, 0, allocate); } int APS5_VABI posix_memalign_nid_postfix(void** pointer, size_t alignment, size_t size) { if (!pointer || alignment < sizeof(void*) || (alignment & (alignment - 1)) != 0) return 22; const int savedError = errno; const int result = ApplicationHeapPosixAlign_nid_no_patch(pointer, alignment, size); - errno = savedError; + const bool failureSetsError = result != 0 && ApplicationHeapDefault_nid_no_patch() && alignment < DefaultHeapAlignmentLimit && + !defaultHeapRequestOverflows(size, alignment); + errno = failureSetsError ? 22 : savedError; return result; } diff --git a/core/libs/prx/libc/src/GuestHeap.cpp b/core/libs/prx/libc/src/GuestHeap.cpp index 96c95d1063..6277174a21 100644 --- a/core/libs/prx/libc/src/GuestHeap.cpp +++ b/core/libs/prx/libc/src/GuestHeap.cpp @@ -280,6 +280,12 @@ void* allocate(GuestAllocations::Mutation& mutation, std::size_t alignment, std: return pointer; } +void* emptyInPlace(GuestAllocations::Mutation& mutation, void* pointer, bool gpu) { + mutation.Remove(pointer); + mutation.Add(pointer, 0, true, true, gpu); + return pointer; +} + void free(GuestAllocations::Mutation& mutation, void* pointer) { mutation.Remove(pointer); const auto blockBytes = reinterpret_cast(pointer)[-1]; @@ -307,10 +313,7 @@ void* GuestHeapReallocate_nid_postfix(void* pointer, std::size_t bytes) { GuestAllocations::Mutation mutation; const auto range = mutation.Find(pointer); mutation.RequireUnpinned(pointer, range.bytes); - if (bytes == 0) { - free(mutation, pointer); - return nullptr; - } + if (bytes == 0) return emptyInPlace(mutation, pointer, range.gpu); void* result = allocate(mutation, MinimumAlignment, bytes, range.gpu); std::memcpy(result, pointer, std::min(bytes, range.bytes)); free(mutation, pointer); @@ -323,8 +326,10 @@ void* GuestHeapRealign_nid_postfix(void* pointer, std::size_t bytes, std::size_t const auto range = mutation.Find(pointer); mutation.RequireUnpinned(pointer, range.bytes); if (bytes == 0) { - free(mutation, pointer); - return nullptr; + if (alignment == 0 || (alignment & (alignment - 1)) != 0) throw std::invalid_argument("invalid guest heap alignment"); + if (reinterpret_cast(pointer) % alignment != 0) + throw std::invalid_argument("reallocalign to 0 bytes with an alignment the block does not have is not measured on the console"); + return emptyInPlace(mutation, pointer, range.gpu); } void* result = allocate(mutation, alignment, bytes, range.gpu); std::memcpy(result, pointer, std::min(bytes, range.bytes)); diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index be99345d50..3f36d640ef 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -303,7 +303,8 @@ int main(int argc, char** argv) { for (unsigned i = 0; i < 63; ++i) require(pointer[i] == 0x5a); pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(pointer, 11)); for (unsigned i = 0; i < 11; ++i) require(pointer[i] == 0x5a); - require(ApplicationHeapReallocate_nid_no_patch(pointer, 0) == nullptr); + require(ApplicationHeapReallocate_nid_no_patch(pointer, 0) == pointer && ApplicationHeapUsableSize_nid_no_patch(pointer) == 0); + ApplicationHeapFree_nid_no_patch(pointer); pointer = static_cast(ApplicationHeapReallocate_nid_no_patch(nullptr, 32)); require(pointer != nullptr); ApplicationHeapFree_nid_no_patch(pointer); @@ -317,7 +318,8 @@ int main(int argc, char** argv) { require(pointer != nullptr && ApplicationHeapUsableSize_nid_no_patch(pointer) == 9); for (unsigned i = 0; i < 9; ++i) require(pointer[i] == 0x3c); *__error_nid_postfix() = 0; - require(reallocf_nid_postfix(pointer, 0) == nullptr && *__error_nid_postfix() == 0); + require(reallocf_nid_postfix(pointer, 0) == pointer && *__error_nid_postfix() == 0 && ApplicationHeapUsableSize_nid_no_patch(pointer) == 0); + ApplicationHeapFree_nid_no_patch(pointer); reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); require(pointer != nullptr); diff --git a/core/libs/tests/GuestAllocatorFailures.cpp b/core/libs/tests/GuestAllocatorFailures.cpp index 5312249a50..ef2ae3d380 100644 --- a/core/libs/tests/GuestAllocatorFailures.cpp +++ b/core/libs/tests/GuestAllocatorFailures.cpp @@ -19,6 +19,7 @@ void* APS5_VABI realloc_nid_postfix(void*, std::size_t); void* APS5_VABI memalign_nid_postfix(std::size_t, std::size_t); void* APS5_VABI aligned_alloc_nid_postfix(std::size_t, std::size_t); int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); +std::size_t APS5_VABI malloc_usable_size_nid_postfix(void*); void APS5_VABI free_nid_postfix(void*); int* APS5_VABI __error_nid_postfix(); } @@ -27,8 +28,10 @@ namespace { constexpr int NoMemory = 12; constexpr int InvalidArgument = 22; +constexpr int Untouched = 77; constexpr std::size_t Max = std::numeric_limits::max(); constexpr std::size_t HalfPlusOne = static_cast(std::numeric_limits::max()) + 1; +constexpr std::size_t LastPlainFailure = Max - 0x1008f; int failures = 0; @@ -50,7 +53,7 @@ void report(const char* what, const char* detail) { template void expectNull(const char* what, int expectedError, TCall call) { - *__error_nid_postfix() = 0; + *__error_nid_postfix() = Untouched; void* result = nullptr; try { result = call(); @@ -95,27 +98,57 @@ int main() { void* api[10]{}; ApplicationHeapRegister_nid_no_patch(api); - expectNull("malloc(SIZE_MAX)", NoMemory, [] { return malloc_nid_postfix(Max); }); - expectNull("malloc(PTRDIFF_MAX + 1)", NoMemory, [] { return malloc_nid_postfix(HalfPlusOne); }); - expectNull("calloc(SIZE_MAX, 2)", NoMemory, [] { return calloc_nid_postfix(Max, 2); }); - expectNull("calloc(2^33, 2^33)", NoMemory, [] { return calloc_nid_postfix(std::size_t{1} << 33, std::size_t{1} << 33); }); - expectNull("calloc(PTRDIFF_MAX + 1, 1)", NoMemory, [] { return calloc_nid_postfix(HalfPlusOne, 1); }); - expectNull("realloc(NULL, SIZE_MAX)", NoMemory, [] { return realloc_nid_postfix(nullptr, Max); }); - expectNull("aligned_alloc(64, SIZE_MAX)", NoMemory, [] { return aligned_alloc_nid_postfix(64, Max); }); - expectNull("memalign(64, PTRDIFF_MAX + 1)", NoMemory, [] { return memalign_nid_postfix(64, HalfPlusOne); }); - expectNull("aligned_alloc(24, 64)", InvalidArgument, [] { return aligned_alloc_nid_postfix(24, 64); }); - expectNull("aligned_alloc(0, 64)", InvalidArgument, [] { return aligned_alloc_nid_postfix(0, 64); }); - expectNull("memalign(24, 64)", InvalidArgument, [] { return memalign_nid_postfix(24, 64); }); + expectNull("malloc(SIZE_MAX)", Untouched, [] { return malloc_nid_postfix(Max); }); + expectNull("malloc(SIZE_MAX - 0x1008e)", Untouched, [] { return malloc_nid_postfix(LastPlainFailure + 1); }); + expectNull("malloc(SIZE_MAX - 0x1008f)", InvalidArgument, [] { return malloc_nid_postfix(LastPlainFailure); }); + expectNull("malloc(PTRDIFF_MAX + 1)", InvalidArgument, [] { return malloc_nid_postfix(HalfPlusOne); }); + expectNull("calloc(SIZE_MAX, 2)", Untouched, [] { return calloc_nid_postfix(Max, 2); }); + expectNull("calloc(2^33, 2^33)", Untouched, [] { return calloc_nid_postfix(std::size_t{1} << 33, std::size_t{1} << 33); }); + expectNull("calloc(1, SIZE_MAX - 0x1008e)", Untouched, [] { return calloc_nid_postfix(1, LastPlainFailure + 1); }); + expectNull("calloc(PTRDIFF_MAX + 1, 1)", InvalidArgument, [] { return calloc_nid_postfix(HalfPlusOne, 1); }); + expectNull("realloc(NULL, SIZE_MAX)", Untouched, [] { return realloc_nid_postfix(nullptr, Max); }); + expectNull("realloc(NULL, PTRDIFF_MAX + 1)", InvalidArgument, [] { return realloc_nid_postfix(nullptr, HalfPlusOne); }); + expectNull("aligned_alloc(64, SIZE_MAX)", Untouched, [] { return aligned_alloc_nid_postfix(64, Max); }); + expectNull("aligned_alloc(16, PTRDIFF_MAX + 1)", InvalidArgument, [] { return aligned_alloc_nid_postfix(16, HalfPlusOne); }); + expectNull("memalign(64, PTRDIFF_MAX + 1)", InvalidArgument, [] { return memalign_nid_postfix(64, HalfPlusOne); }); + expectNull("memalign(4096, SIZE_MAX - 0x110af)", InvalidArgument, [] { return memalign_nid_postfix(4096, Max - 0x110af); }); + expectNull("memalign(4096, SIZE_MAX - 0x110ae)", Untouched, [] { return memalign_nid_postfix(4096, Max - 0x110ae); }); + expectNull("memalign(100, SIZE_MAX - 0x1012f)", InvalidArgument, [] { return memalign_nid_postfix(100, Max - 0x1012f); }); + expectNull("memalign(100, SIZE_MAX - 0x1012e)", Untouched, [] { return memalign_nid_postfix(100, Max - 0x1012e); }); + expectNull("memalign(3, SIZE_MAX - 0x1008f)", InvalidArgument, [] { return memalign_nid_postfix(3, LastPlainFailure); }); + expectNull("memalign(3, SIZE_MAX - 0x1008e)", Untouched, [] { return memalign_nid_postfix(3, LastPlainFailure + 1); }); + expectNull("memalign(2^63, 16)", Untouched, [] { return memalign_nid_postfix(HalfPlusOne, 16); }); + expectNull("memalign(SIZE_MAX, 16)", Untouched, [] { return memalign_nid_postfix(Max, 16); }); + expectNull("memalign(2^62 + 1, 16)", InvalidArgument, [] { return memalign_nid_postfix((std::size_t{1} << 62) + 1, 16); }); + expectNull("aligned_alloc(24, 64)", Untouched, [] { return aligned_alloc_nid_postfix(24, 64); }); + expectNull("aligned_alloc(0, 64)", Untouched, [] { return aligned_alloc_nid_postfix(0, 64); }); + expectNull("aligned_alloc(2^63, 16)", Untouched, [] { return aligned_alloc_nid_postfix(HalfPlusOne, 16); }); + + struct RoundedAlignment { + std::size_t requested; + std::size_t guaranteed; + }; + for (const RoundedAlignment& alignment : {RoundedAlignment{0, 32}, RoundedAlignment{3, 32}, RoundedAlignment{24, 32}, RoundedAlignment{48, 64}, + RoundedAlignment{100, 128}, RoundedAlignment{3000, 4096}}) { + void* aligned = expectBlock("memalign with an alignment that is not a power of two", [&alignment] { return memalign_nid_postfix(alignment.requested, 40); }); + if (aligned != nullptr && reinterpret_cast(aligned) % alignment.guaranteed != 0) report("memalign", "did not round the alignment up to a power of two"); + free_nid_postfix(aligned); + } auto* block = static_cast(expectBlock("malloc(40)", [] { return malloc_nid_postfix(40); })); if (block != nullptr) { std::memset(block, 0x6b, 40); - expectNull("realloc(block, SIZE_MAX)", NoMemory, [block] { return realloc_nid_postfix(block, Max); }); - expectNull("realloc(block, PTRDIFF_MAX + 1)", NoMemory, [block] { return realloc_nid_postfix(block, HalfPlusOne); }); + expectNull("realloc(block, SIZE_MAX)", Untouched, [block] { return realloc_nid_postfix(block, Max); }); + expectNull("realloc(block, PTRDIFF_MAX + 1)", InvalidArgument, [block] { return realloc_nid_postfix(block, HalfPlusOne); }); for (unsigned index = 0; index < 40; ++index) { if (block[index] != 0x6b) report("realloc failure", "damaged the original block"); } - free_nid_postfix(block); + void* emptied = expectBlock("realloc(block, 0)", [block] { return realloc_nid_postfix(block, 0); }); + if (emptied != block) report("realloc(block, 0)", "did not return the same block"); + if (emptied != nullptr && malloc_usable_size_nid_postfix(emptied) != 0) report("realloc(block, 0)", "left usable bytes in the block"); + void* regrown = expectBlock("realloc(emptied, 64)", [emptied] { return realloc_nid_postfix(emptied, 64); }); + if (regrown != nullptr && malloc_usable_size_nid_postfix(regrown) != 64) report("realloc(emptied, 64)", "did not hold 64 bytes"); + free_nid_postfix(regrown); } #ifdef ANYPS5_ENABLE_ASAN @@ -154,8 +187,8 @@ int main() { if (huge != nullptr) { free_nid_postfix(huge); committed = true; - } else if (*__error_nid_postfix() != NoMemory) { - report("malloc(300 GiB)", "returned NULL without ENOMEM"); + } else if (*__error_nid_postfix() != InvalidArgument) { + report("malloc(300 GiB)", "returned NULL without EINVAL"); } else { ++failedCommits; } @@ -177,7 +210,7 @@ int main() { std::fputs("skipped: the host can commit 300 GiB, so the failed commit and arena release checks did not run\n", stderr); #endif } else { - if (failedCommits != 10) report("failed heap commits", "did not return ENOMEM for all ten attempts"); + if (failedCommits != 10) report("failed heap commits", "did not return EINVAL for all ten attempts"); void* after = nullptr; try { after = GuestArena::GuestArenaAllocate_nid_postfix(probeBytes, 0x4000); @@ -198,12 +231,27 @@ int main() { } #endif - void* untouched = &failures; - *__error_nid_postfix() = 77; - if (posix_memalign_nid_postfix(&untouched, HalfPlusOne, 8) != NoMemory) report("posix_memalign(PTRDIFF_MAX + 1)", "did not return ENOMEM"); - if (posix_memalign_nid_postfix(&untouched, 64, HalfPlusOne) != NoMemory) report("posix_memalign(64, PTRDIFF_MAX + 1)", "did not return ENOMEM"); - if (posix_memalign_nid_postfix(&untouched, 64, Max) != NoMemory) report("posix_memalign(64, SIZE_MAX)", "did not return ENOMEM"); - if (untouched != &failures || *__error_nid_postfix() != 77) report("posix_memalign failure", "touched the output or errno"); + struct PosixCase { + const char* what; + std::size_t alignment; + std::size_t size; + int result; + int error; + }; + for (const PosixCase& check : {PosixCase{"posix_memalign(PTRDIFF_MAX + 1, 8)", HalfPlusOne, 8, NoMemory, Untouched}, + PosixCase{"posix_memalign(64, PTRDIFF_MAX + 1)", 64, HalfPlusOne, NoMemory, InvalidArgument}, + PosixCase{"posix_memalign(64, SIZE_MAX)", 64, Max, NoMemory, Untouched}, + PosixCase{"posix_memalign(64, SIZE_MAX - 0x100ef)", 64, Max - 0x100ef, NoMemory, InvalidArgument}, + PosixCase{"posix_memalign(64, SIZE_MAX - 0x100ee)", 64, Max - 0x100ee, NoMemory, Untouched}, + PosixCase{"posix_memalign(4, 8)", 4, 8, InvalidArgument, Untouched}, PosixCase{"posix_memalign(24, 8)", 24, 8, InvalidArgument, Untouched}, + PosixCase{"posix_memalign(PTRDIFF_MAX + 2, 8)", HalfPlusOne + 1, 8, InvalidArgument, Untouched}}) { + void* untouched = &failures; + *__error_nid_postfix() = Untouched; + const int result = posix_memalign_nid_postfix(&untouched, check.alignment, check.size); + if (result != check.result) report(check.what, "returned the wrong error number"); + if (*__error_nid_postfix() != check.error) report(check.what, "left the wrong errno"); + if (untouched != &failures) report(check.what, "touched the output"); + } free_nid_postfix(expectBlock("calloc(3, 5)", [] { return calloc_nid_postfix(3, 5); })); free_nid_postfix(expectBlock("calloc(0, 5)", [] { return calloc_nid_postfix(0, 5); })); diff --git a/core/libs/tests/GuestHeapRounding.cpp b/core/libs/tests/GuestHeapRounding.cpp index d888f7311f..f27c870d0d 100644 --- a/core/libs/tests/GuestHeapRounding.cpp +++ b/core/libs/tests/GuestHeapRounding.cpp @@ -77,14 +77,14 @@ DWORD runChild(const wchar_t* mode) { int runMallocOverflow() { registerDefaultHeap(); - *__error_nid_postfix() = 0; + *__error_nid_postfix() = 77; try { auto* pointer = malloc_nid_postfix(std::numeric_limits::max() - HeaderBytes); if (pointer != nullptr) { free_nid_postfix(pointer); return 1; } - return *__error_nid_postfix() == 12 ? 0 : 4; + return *__error_nid_postfix() == 77 ? 0 : 4; } catch (const std::exception& error) { std::fprintf(stderr, "malloc failed with an unexpected exception: %s\n", error.what()); return 2; @@ -100,14 +100,14 @@ int runReallocOverflow() { auto* original = static_cast(malloc_nid_postfix(bytes)); std::memset(original, 0x5a, bytes); bool rejectedWithExpectedError = false; - *__error_nid_postfix() = 0; + *__error_nid_postfix() = 77; try { auto* result = realloc_nid_postfix(original, std::numeric_limits::max() - HeaderBytes); if (result != nullptr) { free_nid_postfix(result); return 4; } - rejectedWithExpectedError = *__error_nid_postfix() == 12; + rejectedWithExpectedError = *__error_nid_postfix() == 77; } catch (const std::exception& error) { std::fprintf(stderr, "realloc failed with an unexpected exception: %s\n", error.what()); } catch (...) { diff --git a/core/libs/tests/GuestReallocalign.cpp b/core/libs/tests/GuestReallocalign.cpp index fded34f311..cd36081ec0 100644 --- a/core/libs/tests/GuestReallocalign.cpp +++ b/core/libs/tests/GuestReallocalign.cpp @@ -7,6 +7,8 @@ extern "C" { void* APS5_VABI reallocalign_nid_postfix(void*, std::size_t, std::size_t); +std::size_t APS5_VABI malloc_usable_size_nid_postfix(void*); +void APS5_VABI free_nid_postfix(void*); int* APS5_VABI __error_nid_postfix(); } @@ -30,12 +32,43 @@ int main() { auto* shrunk = static_cast(reallocalign_nid_postfix(grown, 16, 16)); Require(shrunk != nullptr && (reinterpret_cast(shrunk) % 16) == 0); for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); - *__error_nid_postfix() = 0; - Require(reallocalign_nid_postfix(shrunk, 16, 0) == nullptr && *__error_nid_postfix() == 22); - *__error_nid_postfix() = 0; - Require(reallocalign_nid_postfix(shrunk, 16, 3) == nullptr && *__error_nid_postfix() == 22); - *__error_nid_postfix() = 0; - Require(reallocalign_nid_postfix(shrunk, std::numeric_limits::max(), 16) == nullptr && *__error_nid_postfix() == 12); + shrunk = static_cast(reallocalign_nid_postfix(shrunk, 16, 0)); + Require(shrunk != nullptr && (reinterpret_cast(shrunk) % 32) == 0); for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); - Require(reallocalign_nid_postfix(shrunk, 0, 16) == nullptr); + shrunk = static_cast(reallocalign_nid_postfix(shrunk, 40, 100)); + Require(shrunk != nullptr && (reinterpret_cast(shrunk) % 128) == 0); + for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); + constexpr std::size_t max = std::numeric_limits::max(); + *__error_nid_postfix() = 77; + Require(reallocalign_nid_postfix(shrunk, max, 16) == nullptr && *__error_nid_postfix() == 77); + Require(reallocalign_nid_postfix(shrunk, max - 0x1008f, 16) == nullptr && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 77; + Require(reallocalign_nid_postfix(shrunk, max - 0x110ae, 4096) == nullptr && *__error_nid_postfix() == 77); + Require(reallocalign_nid_postfix(shrunk, max - 0x110af, 4096) == nullptr && *__error_nid_postfix() == 22); + *__error_nid_postfix() = 77; + Require(reallocalign_nid_postfix(shrunk, 16, std::size_t{1} << 63) == nullptr && *__error_nid_postfix() == 77); + for (int index = 0; index < 16; ++index) Require(shrunk[index] == 0x5a); + Require(reallocalign_nid_postfix(shrunk, 0, 16) == shrunk && malloc_usable_size_nid_postfix(shrunk) == 0); + free_nid_postfix(shrunk); + void* empty = reallocalign_nid_postfix(nullptr, 0, 0); + Require(empty != nullptr); + free_nid_postfix(empty); + for (std::size_t attempt = 0; attempt < 64; ++attempt) { + void* loose = reallocalign_nid_postfix(nullptr, 40, 32); + Require(loose != nullptr); + if (reinterpret_cast(loose) % 4096 == 0) { + free_nid_postfix(loose); + continue; + } + bool threw = false; + try { + reallocalign_nid_postfix(loose, 0, 4096); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + free_nid_postfix(loose); + return 0; + } + throw std::runtime_error("reallocalign check could not get a block without 4096-byte alignment"); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 9179b06e02..d9da36364f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -23,8 +23,8 @@ - [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks. `Memalign` is inferred as `(handle, alignment, size, owner)`, delegates to `sceLibcMspaceMemalign`, and is unverified on console. `Create` has an extra options pointer with a partly observed layout (version, type 1 or 2 at +2, field at +0x10); non-null options throw because the field's meaning is unknown. Allocation owners are ignored because block footers are not modelled. On PS5 firmware 13.60, heap-owner calls worked; wrong owners in `Reallocalign` or in-place `Realloc` terminate the process, and `MallocStats` returns 1 if non-heap-owned blocks exist. These owner results are not reproduced. `PosixMemalign`, `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` remain unimplemented. - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. -- [malloc, calloc, realloc, memalign, aligned_alloc and reallocalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - follow FreeBSD 11.4 `contrib/jemalloc/src/jemalloc.c` for failures, not measured on the console: a size the default heap rejects or the guest arena cannot place (including a `calloc` product that overflows) returns null with `errno` `ENOMEM`, and `memalign` and `aligned_alloc` return null with `EINVAL` for an alignment that is zero or not a power of two. FreeBSD's libc has no `memalign`, so setting `errno` there is assumed. A host commit failure for a block that fits in the arena (Windows `ERROR_NOT_ENOUGH_MEMORY`, `ERROR_OUTOFMEMORY` or `ERROR_COMMITMENT_LIMIT`, Linux `ENOMEM`) resets and releases the reserved range and returns null with `ENOMEM`; any other commit failure releases the range and throws. `reallocalign` returns null with `EINVAL` for an invalid alignment and `ENOMEM` like the others, which is an assumption as it is not in FreeBSD. `realloc(ptr, 0)` frees the block and returns null, where FreeBSD 11.4's jemalloc 4.5 resizes it to one byte and returns a block -- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 frees the block and returns null as `realloc` does here, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` +- [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. `reallocalign(ptr, 0, alignment)` for a block without that alignment was not measured and throws +- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception From 55e4819728bedb167bf652defd20a6f418f77027 Mon Sep 17 00:00:00 2001 From: orezraey Date: Tue, 20 Oct 2026 19:27:16 -0300 Subject: [PATCH 076/318] fix(libc): build reallocf on realloc as FreeBSD's reallocf.c does Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/FileAndHeap.cpp | 11 +++-------- core/libs/tests/ApplicationHeap.cpp | 11 ++++++++--- docs/dev/debt/libc.md | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index d1349613d5..2a2d72e0bd 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -585,14 +585,9 @@ void* APS5_VABI realloc_nid_postfix(void* ptr, size_t newSize) { } void* APS5_VABI reallocf_nid_postfix(void* ptr, size_t newSize) { - try { - return ApplicationHeapReallocate_nid_no_patch(ptr, newSize); - } catch (const std::bad_alloc&) { - } catch (const std::length_error&) { - } - ApplicationHeapFree_nid_no_patch(ptr); - errno = 12; - return nullptr; + void* result = realloc_nid_postfix(ptr, newSize); + if (result == nullptr && ptr != nullptr && newSize != 0) ApplicationHeapFree_nid_no_patch(ptr); + return result; } void* APS5_VABI memalign_nid_postfix(size_t alignment, size_t size) { diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 3f36d640ef..a563e69556 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -323,10 +323,15 @@ int main(int argc, char** argv) { reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); require(pointer != nullptr); - require(reallocf_nid_postfix(pointer, SIZE_MAX - 4096) == nullptr && *__error_nid_postfix() == 12); + *__error_nid_postfix() = 77; + require(reallocf_nid_postfix(pointer, SIZE_MAX - 4096) == nullptr && *__error_nid_postfix() == 77); reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); - *__error_nid_postfix() = 0; - require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 12); + pointer = static_cast(reallocf_nid_postfix(nullptr, 40)); + require(pointer != nullptr); + require(reallocf_nid_postfix(pointer, SIZE_MAX - 0x1008f) == nullptr && *__error_nid_postfix() == 22); + reject([pointer] { ApplicationHeapUsableSize_nid_no_patch(pointer); }); + *__error_nid_postfix() = 77; + require(reallocf_nid_postfix(nullptr, SIZE_MAX) == nullptr && *__error_nid_postfix() == 77); void* empty = ApplicationHeapAllocate_nid_no_patch(0); require(empty != nullptr); ApplicationHeapFree_nid_no_patch(empty); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d9da36364f..561aced31f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -24,7 +24,7 @@ - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. - [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. `reallocalign(ptr, 0, alignment)` for a block without that alignment was not measured and throws -- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails because the application allocator returns null or the default heap rejects the size, the block is freed through the same allocator, `errno` is `ENOMEM` and null is returned. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` +- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails, the block is freed through the same allocator and null is returned with the `errno` that `realloc` left. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception From bb925032c0cdb40a1b5d0c9d42e36c1b4b39a327 Mon Sep 17 00:00:00 2001 From: Nuno Silva <22588915+NFSS10@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:43:00 +0100 Subject: [PATCH 077/318] perf(agc): scope the snapshot spare pool to reused sizes --- .../Graphics/include/ShaderResources.hpp | 3 + .../Graphics/src/ShaderResources.cpp | 22 ++++--- .../tests/execution/SampledCacheBudget.cpp | 63 ++++++++++++++----- 3 files changed, 67 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 382338e94f..cf6765aef0 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -36,9 +36,12 @@ std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memor bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t StorageTextureCacheBudget(const Context& context); +inline constexpr std::uint64_t SpareSnapshotPresents = 16; struct TextureCacheUse { std::size_t sampledEntries = 0; + std::size_t spareEntries = 0; std::uint64_t sampledBytes = 0; + std::uint64_t spareBytes = 0; std::uint64_t storageBytes = 0; }; TextureCacheUse TextureCacheUsage(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d0bb065ef3..d2ebc31097 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -131,7 +131,7 @@ struct TextureCache { std::list entries; std::unordered_map::iterator, TextureKeyHash> index; std::unordered_multimap::iterator> views; - std::vector> spare; + std::vector, std::uint64_t>> spare; std::uint64_t spareBytes = 0; std::uint64_t bytes = 0; std::size_t passed = 0; @@ -267,27 +267,33 @@ void touchTexture(TextureCache& cache, std::list::iterator it) { it->present = Recorder::Presents(); } -constexpr std::uint64_t SpareSnapshotBytes = 512ull << 20; +constexpr std::uint64_t SpareSnapshotBytes = 256ull << 20; void parkSnapshot(TextureCache& cache, std::vector& bytes) { const auto size = bytes.size(); if (size == 0) return; - while (!cache.spare.empty() && cache.spareBytes + size > SpareSnapshotBytes) { - cache.spareBytes -= cache.spare.front().size(); + const auto present = Recorder::Presents(); + while (!cache.spare.empty() && (present - cache.spare.front().second > SpareSnapshotPresents || cache.spareBytes + size > SpareSnapshotBytes)) { + cache.spareBytes -= cache.spare.front().first.size(); cache.spare.erase(cache.spare.begin()); } if (cache.spareBytes + size <= SpareSnapshotBytes) { cache.spareBytes += size; - cache.spare.push_back(std::move(bytes)); + cache.spare.emplace_back(std::move(bytes), present); } bytes = std::vector{}; } std::vector spareSnapshot(TextureCache& cache, std::size_t bytes) { + const auto present = Recorder::Presents(); + while (!cache.spare.empty() && present - cache.spare.front().second > SpareSnapshotPresents) { + cache.spareBytes -= cache.spare.front().first.size(); + cache.spare.erase(cache.spare.begin()); + } for (auto spare = cache.spare.begin(); spare != cache.spare.end(); ++spare) { - if (spare->size() != bytes) continue; + if (spare->first.size() != bytes) continue; cache.spareBytes -= bytes; - std::vector taken = std::move(*spare); + std::vector taken = std::move(spare->first); cache.spare.erase(spare); return taken; } @@ -1280,6 +1286,8 @@ TextureCacheUse TextureCacheUsage() { dropUncachedViews(sampled); use.sampledEntries = sampled.entries.size(); use.sampledBytes = sampled.bytes; + use.spareEntries = sampled.spare.size(); + use.spareBytes = sampled.spareBytes; } use.storageBytes = storageCacheBytes(); return use; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 51b16af243..7a0efc3c5b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -281,38 +281,38 @@ private: class WatchedSurface { public: - explicit WatchedSurface(const Context& context) : context(context) { + explicit WatchedSurface(const Context& context, std::size_t bytes = SurfaceBytes) : context(context), span(bytes) { #ifdef _WIN32 - memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, 65536)); - if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); + memory = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (memory != nullptr) GuestArena::GuestArenaCommit_nid_postfix(memory, bytes, PAGE_READWRITE, bytes); #else constexpr std::uintptr_t alignment = 65536; - void* mapped = mmap(nullptr, SurfaceBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + void* mapped = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (mapped != MAP_FAILED) { const auto begin = reinterpret_cast(mapped); const auto aligned = (begin + alignment - 1) & ~(alignment - 1); if (aligned != begin) munmap(mapped, aligned - begin); - if (aligned + SurfaceBytes != begin + SurfaceBytes + alignment) munmap(reinterpret_cast(aligned + SurfaceBytes), begin + alignment - aligned); + if (aligned + bytes != begin + bytes + alignment) munmap(reinterpret_cast(aligned + bytes), begin + alignment - aligned); memory = reinterpret_cast(aligned); - GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory, bytes); } #endif Require(memory != nullptr, "cannot allocate the watched surface"); - Require(AgcDriver::GuestMemory::Watched(Address(), SurfaceBytes), "the watched surface is not write-watched"); - std::memset(memory, 0x5a, SurfaceBytes); - GuestAllocations::Mutation().Add(memory, SurfaceBytes, true, true, true); - AgcDriver::GuestMemory::CollectWritesUncached(Address(), SurfaceBytes); + Require(AgcDriver::GuestMemory::Watched(Address(), bytes), "the watched surface is not write-watched"); + std::memset(memory, 0x5a, bytes); + GuestAllocations::Mutation().Add(memory, bytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(Address(), bytes); } ~WatchedSurface() { ClearCachedTextures(context.device); GuestAllocations::Mutation().Remove(memory); #ifdef _WIN32 - GuestArena::GuestArenaReset_nid_postfix(memory, SurfaceBytes); - GuestArena::GuestArenaRelease_nid_postfix(memory, SurfaceBytes); + GuestArena::GuestArenaReset_nid_postfix(memory, span); + GuestArena::GuestArenaRelease_nid_postfix(memory, span); #else - munmap(memory, SurfaceBytes); - GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, SurfaceBytes); + munmap(memory, span); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, span); #endif } @@ -323,6 +323,7 @@ public: private: const Context& context; + std::size_t span = 0; std::uint8_t* memory = nullptr; }; @@ -686,6 +687,39 @@ void recycleTests(const Context& context) { const auto third = Sampled(context, address); Require(third != second, "the replaced snapshot's size-equal buffer was not reread from guest memory"); Require(Sampled(context, address) == third, "an unchanged surface did not serve its remade snapshot"); + constexpr std::uint32_t BigSide = Side * 2; + constexpr std::size_t BigBytes = std::size_t{BigSide} * BigSide * 4u; + const WatchedSurface watchedFirst(context, BigBytes); + const WatchedSurface watchedSecond(context, BigBytes); + const auto bigFirst = Sampled(context, watchedFirst.Address(), BigSide); + Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the first big snapshot did not park its dropped " + std::to_string(BigBytes) + "-byte buffer in the spare pool"); + const auto bigSecond = Sampled(context, watchedSecond.Address(), BigSide); + Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the second big snapshot did not take the parked spare of its size"); +} + +void spareAgeTests(const Context& context) { + Block block(context); + ClearCachedTextures(context.device); + constexpr std::uint32_t BigSide = Side * 2; + constexpr std::size_t BigBytes = std::size_t{BigSide} * BigSide * 4u; + { + const WatchedSurface watched(context, BigBytes); + const auto first = Sampled(context, watched.Address(), BigSide); + Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the big snapshot did not park its dropped buffer in the spare pool"); + for (std::uint64_t present = 0; present <= SpareSnapshotPresents; ++present) Recorder::CountPresent(); + const auto small = Sampled(context, block.Surface(0)); + Require(TextureCacheUsage().spareEntries == 0 && TextureCacheUsage().spareBytes == 0, "a spare idle for " + std::to_string(SpareSnapshotPresents + 1u) + " presents was not released"); + } + ClearCachedTextures(context.device); + { + const WatchedSurface watchedFirst(context, BigBytes); + const WatchedSurface watchedSecond(context, BigBytes); + const auto second = Sampled(context, watchedFirst.Address(), BigSide); + Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "the second big snapshot did not park its dropped buffer in the spare pool"); + for (std::uint64_t present = 0; present < SpareSnapshotPresents; ++present) Recorder::CountPresent(); + const auto third = Sampled(context, watchedSecond.Address(), BigSide); + Require(TextureCacheUsage().spareEntries == 1 && TextureCacheUsage().spareBytes == BigBytes, "a spare taken within " + std::to_string(SpareSnapshotPresents) + " presents was released instead of reused"); + } } void heldTests(const Context& context) { @@ -1114,6 +1148,7 @@ int main(int argc, char** argv) { secondUseTests(context); earlyMissTests(context); collidingKeyTests(context); + spareAgeTests(context); ClearCachedTextures(context.device); } std::puts("sampled texture cache budget tests passed"); From eb04828a69cb72faab780b37a9d05017f716b52a Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:50:55 +0300 Subject: [PATCH 078/318] perf(shader): use table-driven genericFormattedLoad for descriptor buffer loads --- .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index aa7dc2bbe8..8ce3cc9ca7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -466,10 +466,15 @@ std::uint32_t genericFormattedLoad(SpirvValueEmitContext& context, const IrValue values[component] = EmitValueOrZeroIfCondition(state, both(state, inBounds, required[component]), [&] { const auto width = Select(state, u32, packed, constant(32u), bits[component]); const auto dword = equal(state, width, constant(32u)); - const auto raw = EmitValueIfElse(state, dword, u32, [&] { return readBuffer(context, instruction, addresses[component].guest, 32u); }, [&] { - return EmitBdaReadBytes(context, instruction, addresses[component].guest, Binary(state, spv::OpShiftRightLogical, u32, width, constant(3u))); - }); - const auto bitOffset = Select(state, u32, packed, field(state, bitOffsets, component * 8u, 8u), constant(0u)); + const auto raw = memory.gpuDescriptor + ? EmitValueIfElse(state, dword, u32, [&] { return readBuffer(context, instruction, addresses[component].guest, 32u); }, [&] { + return EmitBdaReadBytes(context, instruction, addresses[component].guest, Binary(state, spv::OpShiftRightLogical, u32, width, constant(3u))); + }) + : readBuffer(context, instruction, addresses[component].guest, 32u); + const auto subwordOffset = memory.gpuDescriptor + ? constant(0u) + : Binary(state, spv::OpShiftLeftLogical, u32, EmitAndConstant(state, addresses[component].guest, 3u), constant(3u)); + const auto bitOffset = Select(state, u32, packed, field(state, bitOffsets, component * 8u, 8u), subwordOffset); const auto unsignedField = EmitBitFieldUExtract(state, raw, bitOffset, bits[component]); const auto signedField = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldSExtract, TypeI32(state), signedField, Unary(state, spv::OpBitcast, TypeI32(state), raw), bitOffset, bits[component]); @@ -539,15 +544,15 @@ std::uint32_t formatSwitch(SpirvValueEmitContext& context, const IrValue& instru const auto& memory = context.Memory(instruction); const auto format = field(state, resource.word3, 12u, 7u); constexpr std::uint32_t count = static_cast(IrBufferFormat::Format32_32_32_32Float); - if (!store && memory.gpuDescriptor) { + if (!store) { const auto type = components == 1u ? TypeU32(state) : TypeU32Composite(state, components); const auto zero = components == 1u ? ConstantU32(state, 0u) : ConstantU32CompositeZero(state, components); const auto unsupported = Binary(state, spv::OpLogicalOr, TypeBool(state), equal(state, format, ConstantU32(state, 0u)), Binary(state, spv::OpUGreaterThan, TypeBool(state), format, ConstantU32(state, count))); const auto result = EmitValueIfElse(state, unsupported, type, [&] { - RecordBdaFault(state, resource.base, ConstantU32(state, 0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::InvalidDescriptor); + if (memory.gpuDescriptor) RecordBdaFault(state, resource.base, ConstantU32(state, 0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::InvalidDescriptor); return zero; }, [&] { return genericFormattedLoad(context, instruction, resource, components, format, count); }); - StopBdaInvocationIf(state, unsupported); + if (memory.gpuDescriptor) StopBdaInvocationIf(state, unsupported); return result; } const auto merge = state.module.AllocateId(); From e3b24e45b13a554bba87a7489b51f1a2c7139b04 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:51:02 +0300 Subject: [PATCH 079/318] perf(shader): treat OpExtInst as pure in SpecializeSpirv dead-code elimination --- .../recompiler/SpirvBackend/src/SpirvSpecialization.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index d77a063d72..2efa6064de 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -203,6 +203,7 @@ bool PureInstruction(const Instruction& instruction) { case spv::OpInBoundsPtrAccessChain: case spv::OpArrayLength: case spv::OpPhi: + case spv::OpExtInst: return true; case spv::OpLoad: return instruction.size() == 4u || (instruction.size() == 5u && instruction[4] == spv::MemoryAccessMaskNone); @@ -216,6 +217,9 @@ bool VisitInputs(const Instruction& instruction, const TVisitor& visit) { std::size_t first = 3u; std::size_t end = instruction.size(); switch (Opcode(instruction)) { + case spv::OpExtInst: + first = 5u; + break; case spv::OpVectorShuffle: case spv::OpCompositeInsert: end = 5u; From cfc2c5ff0f47ba7714e3fd2e107b2ee9bca0da1f Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:51:09 +0300 Subject: [PATCH 080/318] perf(shader): fold constant private table loads and logical operations in SpecializeSpirv --- .../SpirvBackend/src/SpirvSpecialization.cpp | 69 ++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 2efa6064de..a2ef8729d9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -584,6 +584,8 @@ public: vectorTypes.Reset(bound); removed.Reset(bound); aliases.Reset(bound); + composites.Reset(bound); + privateTables.Reset(bound); for (std::size_t cursor = 5; cursor < words.size();) { const auto count = words[cursor] >> 16u; if (count == 0u || count > words.size() - cursor) throw std::runtime_error("truncated prepared SPIR-V instruction"); @@ -600,8 +602,25 @@ public: if (op == spv::OpConstant && types.contains(instruction.at(1)) && types.at(instruction[1]).width <= 32u) values.emplace(instruction.at(2), instruction.at(3)); if (op == spv::OpConstantTrue || op == spv::OpConstantFalse) values.emplace(instruction.at(2), op == spv::OpConstantTrue ? 1u : 0u); if ((op == spv::OpConstant || op == spv::OpConstantTrue || op == spv::OpConstantFalse) && values.contains(instruction.at(2))) canonical.emplace(std::pair{instruction.at(1), values.at(instruction[2])}, instruction[2]); + if (op == spv::OpConstantComposite && instruction.size() >= 3u) composites.emplace(instruction.at(2), std::vector(instruction.begin() + 3, instruction.end())); + if (op == spv::OpVariable && instruction.size() == 5u && instruction[3] == spv::StorageClassPrivate && composites.contains(instruction[4])) privateTables.emplace(instruction.at(2), instruction[4]); cursor += count; } + IdTable tablePointers; + tablePointers.Reset(bound); + for (const auto& instruction : instructions) { + const auto op = Opcode(instruction); + if ((op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) && instruction.size() == 5u && privateTables.contains(instruction[3])) { + tablePointers.emplace(instruction[2], instruction[3]); + continue; + } + if (op == spv::OpLoad && instruction.size() == 4u && tablePointers.contains(instruction[3])) continue; + if (op == spv::OpName || op == spv::OpDecorate || op == spv::OpVariable || op == spv::OpEntryPoint) continue; + for (std::size_t index = 1; index < instruction.size(); ++index) { + privateTables.erase(instruction[index]); + if (const auto* table = tablePointers.find(instruction[index])) privateTables.erase(*table); + } + } } std::vector Run() { @@ -641,12 +660,30 @@ private: return found != nullptr && *found == type; } - std::optional evaluate(const Instruction& instruction) const { + std::optional evaluate(const Instruction& instruction, const IdTable>& constPointers) const { const auto op = Opcode(instruction); if (instruction.size() < 4u) return std::nullopt; const auto* type = types.find(instruction[1]); if (type == nullptr || type->width > 32u) return std::nullopt; + if (op == spv::OpLoad && instruction.size() == 4u) { + const auto* pointer = constPointers.find(instruction[3]); + if (pointer == nullptr) return std::nullopt; + const auto& elements = composites.at(pointer->first); + return pointer->second < elements.size() ? value(elements[pointer->second]) : std::nullopt; + } const auto left = value(instruction[3]); + if (op == spv::OpLogicalAnd && instruction.size() == 5u) { + const auto right = value(instruction[4]); + if ((left && *left == 0u) || (right && *right == 0u)) return 0u; + if (left && right) return *left != 0u && *right != 0u; + return std::nullopt; + } + if (op == spv::OpLogicalOr && instruction.size() == 5u) { + const auto right = value(instruction[4]); + if ((left && *left != 0u) || (right && *right != 0u)) return 1u; + if (left && right) return *left != 0u || *right != 0u; + return std::nullopt; + } if (!left) return std::nullopt; if (op == spv::OpCopyObject) return left; if (op == spv::OpLogicalNot) return *left == 0u; @@ -694,16 +731,42 @@ private: extracts.Reset(bound); IdTable bitcasts; bitcasts.Reset(bound); + IdTable> constPointers; + constPointers.Reset(bound); for (auto& instruction : instructions) { if (instruction.empty()) continue; auto op = Opcode(instruction); if (op == spv::OpFunction) function = true; if (op == spv::OpFunctionEnd) function = false; if (!function) continue; + if ((op == spv::OpAccessChain || op == spv::OpInBoundsAccessChain) && instruction.size() == 5u && privateTables.contains(instruction[3])) { + if (const auto index = value(instruction[4])) constPointers.emplace(instruction[2], std::pair{privateTables.at(instruction[3]), *index}); + } if (op == spv::OpSelect && instruction.size() == 6u) { if (const auto condition = value(instruction[3])) { instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[*condition != 0u ? 4u : 5u]}); changed = true; + } else if (instruction[4] == instruction[5]) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[4]}); + changed = true; + } + } + if (op == spv::OpLogicalAnd && instruction.size() == 5u) { + if (const auto left = value(instruction[3]); left && *left != 0u) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[4]}); + changed = true; + } else if (const auto right = value(instruction[4]); right && *right != 0u) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[3]}); + changed = true; + } + } + if (op == spv::OpLogicalOr && instruction.size() == 5u) { + if (const auto left = value(instruction[3]); left && *left == 0u) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[4]}); + changed = true; + } else if (const auto right = value(instruction[4]); right && *right == 0u) { + instruction = Make(spv::OpCopyObject, {instruction[1], instruction[2], instruction[3]}); + changed = true; } } if (op == spv::OpVectorExtractDynamic && instruction.size() == 5u) { @@ -746,7 +809,7 @@ private: } } if (op == spv::OpCopyObject && values.contains(instruction.at(2))) continue; - const auto result = evaluate(instruction); + const auto result = evaluate(instruction, constPointers); if (!result) continue; const auto type = types.at(instruction[1]); const auto bits = type.width == 32u ? *result : *result & ((1u << type.width) - 1u); @@ -1096,6 +1159,8 @@ private: IdSet vectorTypes; IdSet removed; IdTable aliases; + IdTable> composites; + IdTable privateTables; std::map, std::uint32_t> canonical; }; From 780284c150a918f6e2bc9ea1993a976bd3b32944 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 01:51:16 +0300 Subject: [PATCH 081/318] perf(shader): drop redundant post-compaction spirv-opt pass in buildSpecializedModule --- core/shader/recompiler/Recompiler.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 9246de6bda..0d76ccb36d 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -819,9 +819,6 @@ std::shared_ptr buildSpecializedModule(const CompiledSh cursor += count; } materialized = std::move(compact); -#if ANYPS5_ENABLE_SPIRV_TOOLS - materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false); -#endif auto module = std::make_shared(); std::map bindingNumbers; for (std::size_t cursor = 5; cursor < materialized.size();) { From b456725205d5ebd82ac8b7804617425ce2476432 Mon Sep 17 00:00:00 2001 From: teepean Date: Wed, 21 Oct 2026 02:01:57 +0300 Subject: [PATCH 082/318] perf(agc): skip an address space's pending CPU write scan when none are pending GuestBufferMemory::completeSpaceWrites ran at every upload of an address-based build: it copied every pending write range of the open and in-flight batches, searched each one in the space's sorted regions and asked CompletePendingCpuWrites about every region it overlapped. That call returns false at once when no copied writer and no completion label is pending, so without one the whole scan found nothing. The scan now starts only when AnyPendingCpuWrites says one of them is pending. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp | 2 +- .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index c0d65d1f74..a74db39947 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -46,6 +46,7 @@ void OverrideCacheBudgetForTest(std::uint64_t bytes); std::uint64_t SampledTextureCacheBudget(const Context& context); std::shared_ptr CachedSampledTexture(const Context& context, std::span words, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D); bool SampledTexturesShareEntry(std::span first, std::span second); +bool AnyPendingCpuWrites(const Context& context); bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes); bool SpirvStoresThroughAddresses(std::span words); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 82d2b10a04..1a9d1028cc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -4529,7 +4529,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) { void GuestBufferMemory::completeSpaceWrites() const { auto* recorder = Recorder::Active(); - if (recorder == nullptr || Recorder::InCompletion()) return; + if (recorder == nullptr || Recorder::InCompletion() || !AnyPendingCpuWrites(context)) return; const auto& base = space->base; std::vector> writes; std::vector candidates; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index c75781da18..95dd210ed1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3233,6 +3233,11 @@ ShaderResources::OwnRefreshFallback ShaderResources::refreshOwnObjects(std::span return OwnRefreshFallback::Count; } +bool AnyPendingCpuWrites(const Context& context) { + if (Recorder::InCompletion() || Recorder::Active() == nullptr) return false; + return (context.copiedWriters != nullptr && !context.copiedWriters->empty()) || !DrawCopiedWriters()->empty() || Recorder::PendingCompletionLabels() != 0; +} + bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes) { if (bytes == 0 || Recorder::InCompletion()) return false; auto* recorder = Recorder::Active(); From 966018783f0644f601b747b6e22e3ffee9cbe760 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 12:13:06 +1300 Subject: [PATCH 083/318] fix(agc): keep the registered-stream throw for the workload calls Review on #8302: the ACB workload calls and the DCB stream-inactive call throw again for a registered stream until a title is shown stopping on them (as #3789 did for the DCB calls, and as #4709 kept). The debt entry keeps the firmware's packet layout as the record of what is missing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceAgc/Acb/src/Control.cpp | 21 ++++++-- .../libSceAgc/DcbState/include/Workload.hpp | 11 ---- .../prx/libSceAgc/DcbState/src/Workload.cpp | 50 ++++++------------- core/libs/prx/libSceAgc/tests/Commands.cpp | 17 +++---- docs/dev/debt/libSceAgc.md | 2 +- 5 files changed, 40 insertions(+), 61 deletions(-) diff --git a/core/libs/prx/libSceAgc/Acb/src/Control.cpp b/core/libs/prx/libSceAgc/Acb/src/Control.cpp index 539066325a..567bfd5e18 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Control.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Control.cpp @@ -4,13 +4,23 @@ #include "prx/libSceAgc/Command/include/Memory.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include "prx/libSceAgc/DcbState/include/Marker.hpp" -#include "prx/libSceAgc/DcbState/include/Workload.hpp" #include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp" +#include "prx/libSceAgcDriver/Resource/include/Registration.hpp" #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +std::uint32_t* RejectUnregisteredWorkloadStream(CommandBuffer* buf, std::uint32_t streamId, const char* function) { + if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + NotImplemented_nid_no_patch(function); + return nullptr; +} + +} + extern "C" { std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) { @@ -73,15 +83,18 @@ std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t vide } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) { - return Agc::Workload::Active(buf, streamId, workloadIds, workloadCount, __func__); + (void)workloadIds; + (void)workloadCount; + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId) { - return Agc::Workload::Complete(buf, streamId, workloadId, __func__); + (void)workloadId; + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); } std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId) { - return Agc::Workload::StreamInactive(buf, streamId, __func__); + return RejectUnregisteredWorkloadStream(buf, streamId, __func__); } uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uint32_t color) { diff --git a/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp b/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp index 75ae9c3979..3d5d6cac71 100644 --- a/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp +++ b/core/libs/prx/libSceAgc/DcbState/include/Workload.hpp @@ -1,15 +1,4 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_WORKLOAD_HPP #define CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_WORKLOAD_HPP -#include -#include "SceTypes.hpp" - -namespace Agc::Workload { - -std::uint32_t* Active(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount, const char* function); -std::uint32_t* Complete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId, const char* function); -std::uint32_t* StreamInactive(CommandBuffer* buf, std::uint32_t streamId, const char* function); - -} - #endif diff --git a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp index 06d5578a81..0f739d9055 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Workload.cpp @@ -13,7 +13,6 @@ namespace { constexpr std::uint32_t OpcodeNop = 0x10; constexpr std::uint32_t ActivePacketWords = 18; constexpr std::uint32_t CompletePacketWords = 12; -constexpr std::uint32_t InactivePacketWords = 9; constexpr std::uint32_t MaxWorkloadId = 63; constexpr std::uint32_t MaxActiveCount = 63; @@ -26,57 +25,40 @@ uint32_t* NewPacket(CommandBuffer* buf, std::uint32_t words, const char* functio } -namespace Agc::Workload { +extern "C" { -std::uint32_t* Complete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId, const char* function) { - if (buf == nullptr || workloadId > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; - const std::uint64_t clearMask = ~(std::uint64_t{1} << workloadId); - auto* packet = NewPacket(buf, CompletePacketWords, function); - packet[1] = streamId; - packet[2] = workloadId; +uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { + if (buf == nullptr || workload_id > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; + const std::uint64_t clearMask = ~(std::uint64_t{1} << workload_id); + auto* packet = NewPacket(buf, CompletePacketWords, __func__); + packet[1] = stream_id; + packet[2] = workload_id; packet[3] = static_cast(clearMask); packet[4] = static_cast(clearMask >> 32); return packet; } -std::uint32_t* Active(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount, const char* function) { - if (buf == nullptr || workloadIds == nullptr || workloadCount == 0 || workloadCount > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; +uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { + if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr; std::uint64_t mask = 0; - for (std::uint32_t index = 0; index < workloadCount; ++index) { - const auto id = workloadIds[index]; + for (std::uint32_t index = 0; index < workload_count; ++index) { + const auto id = workload_ids[index]; if (id > MaxWorkloadId) return nullptr; const std::uint64_t bit = std::uint64_t{1} << id; if ((mask & bit) != 0) return nullptr; mask |= bit; } - auto* packet = NewPacket(buf, ActivePacketWords, function); - packet[1] = streamId; + auto* packet = NewPacket(buf, ActivePacketWords, __func__); + packet[1] = stream_id; packet[2] = static_cast(mask); packet[3] = static_cast(mask >> 32); return packet; } -std::uint32_t* StreamInactive(CommandBuffer* buf, std::uint32_t streamId, const char* function) { - if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; - auto* packet = NewPacket(buf, InactivePacketWords, function); - packet[1] = streamId; - return packet; -} - -} - -extern "C" { - -uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) { - return Agc::Workload::Complete(buf, stream_id, workload_id, __func__); -} - -uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) { - return Agc::Workload::Active(buf, stream_id, workload_ids, workload_count, __func__); -} - uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) { - return Agc::Workload::StreamInactive(buf, streamId, __func__); + if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr; + NotImplemented_nid_no_patch(__func__); + return nullptr; } } diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index cbf13610e2..960dbb36cc 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -294,17 +294,12 @@ void testWorkloadStreams() { check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed"); Storage registered; registered.words.fill(0xdeadbeefu); - const auto* inactive = sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); - check(inactive == registered.words.data() && inactive[0] == Agc::Command::Header(0x10, 9, 0) && inactive[1] == 1, "DCB workload stream inactive packet"); - for (std::uint32_t word = 2; word < 9; ++word) check(inactive[word] == 0, "DCB workload stream inactive packet tail is not zero"); - check(registered.buffer.cursor_up == registered.words.data() + 9, "DCB workload stream inactive cursor advance"); - Storage dcb; - Storage acb; - check(sceAgcDcbSetWorkloadsActive(&dcb.buffer, 1, ids, 2) != nullptr && sceAgcAcbSetWorkloadsActive(&acb.buffer, 1, ids, 2) == acb.words.data(), "ACB workloads active packet"); - check(sceAgcDcbSetWorkloadComplete(&dcb.buffer, 1, 3) != nullptr && sceAgcAcbSetWorkloadComplete(&acb.buffer, 1, 3) == acb.words.data() + 18, "ACB workload complete packet"); - check(sceAgcDcbSetWorkloadStreamInactive(&dcb.buffer, 1) != nullptr && sceAgcAcbSetWorkloadStreamInactive(&acb.buffer, 1) == acb.words.data() + 30, "ACB workload stream inactive packet"); - check(acb.buffer.cursor_up == acb.words.data() + 39 && dcb.words == acb.words, "ACB workload packets differ from the DCB ones"); - check(sceAgcAcbSetWorkloadsActive(&acb.buffer, 1, ids, 0) == nullptr && sceAgcAcbSetWorkloadComplete(&acb.buffer, 1, 64) == nullptr, "ACB workload argument checks"); + const auto untouched = registered.words; + expectFailure([&] { sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); }); + expectFailure([&] { sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2); }); + expectFailure([&] { sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 3); }); + expectFailure([&] { sceAgcAcbSetWorkloadStreamInactive(®istered.buffer, 1); }); + check(registered.words == untouched && registered.buffer.cursor_up == registered.words.data(), "a workload call that threw modified the buffer"); check(sceAgcDcbSetWorkloadStreamInactive(nullptr, 1) == nullptr && sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr && sceAgcAcbSetWorkloadComplete(nullptr, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(nullptr, 1) == nullptr, "a workload call with a null command buffer for a registered stream returned a packet"); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index d9c6839824..7725620d8f 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -42,6 +42,6 @@ - [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents - [sceAgcSetAmmSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the `(memory, sizeInBytes)` and `(index, label)` signatures, the 16 KiB alignment of the memory and its size, the single registration, the 32-byte labels and the zeroed first 8 bytes of each are what [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L508-L546) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Agc/AgcExports.AmmSemaphore.cs) agree on; KytyPS5 has neither function. OpenAGC clears the whole memory and sharpemu only those 8 bytes, so a registration whose labels hold data past them throws. The two return different errors for every rejected call (OpenAGC `0x8a6c0002`, `0x8a6c000b`, `0x8a6c0048` and `0x8a6c0049`, sharpemu generic invalid-argument and already-exists errors), so every rejected call throws, including a lookup before registration. Not measured on the console; PPSA02801 imports both (OpenAGC's import analysis), no title is shown calling them. -- [sceAgcDcbSetWorkloadsActive, sceAgcDcbSetWorkloadComplete, sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp), [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the console packets are not written. The firmware builds them with libSceAgcDriver's sceAgcDriverSetWorkloadsActive, sceAgcDriverSetWorkloadComplete and sceAgcDriverSetWorkloadStreamInactive: a SET_UCONFIG_REG of a tag to 0x342 (`0xCC000000 | stream` for active and inactive, `0xCD000000 + workload * 32 + stream` for complete) that carries the first dword of an 8-byte payload (the active mask, zero for inactive), a WRITE_DATA of the rest to register 0xC343, and for active and complete a 9-dword ATOMIC_MEM that ORs the mask into, or ANDs the cleared bit out of, the stream's 64-bit word in a table the driver takes at module start (control 0x267 or 0x275, bit 30 set on the DCB). The executor has no ATOMIC_MEM, the table is not allocated, and an unpredicated SET_UCONFIG_REG is rejected on a compute queue, so the calls write an `IT_NOP` of the console size instead: 18 dwords for active (stream id, mask), 12 for complete (stream id, workload id, clear mask) and 9 for inactive (stream id); the ACB calls write the same packets as the DCB calls. The argument checks are the firmware's: null for a null command buffer, a stream outside 1 to 31 or not registered, a workload id above 63, and for active a null id list, a duplicate id or a count of 0 or above 63; the stream is checked before the ids are read. The driver registration records only the stream id; the console also keeps a 32-byte name per stream and pre-registers stream 0 as "System", and its error codes for an invalid or repeated registration are not returned, so registration never fails here. +- [sceAgcDcbSetWorkloads*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console packets are not written. DCB active and complete write a console-sized `IT_NOP` (18 and 12 dwords, [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) layout); DCB stream inactive and the three ACB calls throw for a registered stream. The firmware writes, through the driver's SetWorkload* builders, a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete), a WRITE_DATA to 0xC343, and for active and complete an ATOMIC_MEM on a per-stream 64-bit mask in a driver table. The executor has no ATOMIC_MEM or table. Registration records only the stream id and never fails - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From efc3bd2efd1621ec6670a4604e68aa4fcaa5aad9 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 02:12:57 +0300 Subject: [PATCH 084/318] test(shader): cover private table specialization folding and subword formatted loads --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 81 +++++++++++++++++ .../libSceAgcDriver/tests/SpirvOptimizer.cpp | 85 +++++++++++++++++ .../tests/execution/BufferFormat.cpp | 91 +++++++++++++++++++ .../SpirvBackend/src/SpirvSpecialization.cpp | 11 ++- 4 files changed, 265 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 1be75623e3..00796cd2b5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -298,6 +298,86 @@ void testIdsBeyondTheBoundAreRejected() { check(rejected, "a module whose ids reach its bound was specialized"); } +void testPrivateCompositeTableLoadFolding() { + for (const auto accessOp : {spv::OpAccessChain, spv::OpInBoundsAccessChain}) { + const auto build = [&](bool dynamicIndex, bool storeThroughChain) { + SpirvModule module; + module.EmitCapability(spv::CapabilityShader); + const auto typeVoid = module.Type(spv::OpTypeVoid); + const auto typeBool = module.Type(spv::OpTypeBool); + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto falseVal = module.Constant(spv::OpConstantFalse, typeBool); + const auto zero = module.Constant(spv::OpConstant, typeU32, 0u); + const auto one = module.Constant(spv::OpConstant, typeU32, 1u); + const auto three = module.Constant(spv::OpConstant, typeU32, 3u); + const auto sixteen = module.Constant(spv::OpConstant, typeU32, 16u); + const auto twenty = module.Constant(spv::OpConstant, typeU32, 20u); + const auto thirty = module.Constant(spv::OpConstant, typeU32, 30u); + const auto typeArray = module.Type(spv::OpTypeArray, typeU32, three); + const auto initial = module.Constant(spv::OpConstantComposite, typeArray, std::vector{sixteen, twenty, thirty}); + const auto typeInputPtr = module.Type(spv::OpTypePointer, spv::StorageClassInput, typeU32); + const auto typeTablePtr = module.Type(spv::OpTypePointer, spv::StorageClassPrivate, typeArray); + const auto typePrivatePtr = module.Type(spv::OpTypePointer, spv::StorageClassPrivate, typeU32); + const auto typeFunction = module.Type(spv::OpTypeFunction, typeVoid); + const auto input = module.DefineGlobalVariable(typeInputPtr, spv::StorageClassInput); + const auto table = module.DefineInitializedGlobalVariable(typeTablePtr, spv::StorageClassPrivate, initial); + const auto output = module.DefineGlobalVariable(typePrivatePtr, spv::StorageClassPrivate); + const auto mainId = module.AllocateId(); + const auto lane = module.AllocateId(); + const auto rawCond = module.AllocateId(); + const auto cond = module.AllocateId(); + const auto baseIndex = module.AllocateId(); + const auto index = module.AllocateId(); + const auto element = module.AllocateId(); + const auto loaded = module.AllocateId(); + const auto sum = module.AllocateId(); + module.AddFunction(spv::OpFunction, typeVoid, mainId, spv::FunctionControlMaskNone, typeFunction); + module.AddFunction(spv::OpLabel, module.AllocateId()); + module.AddFunction(spv::OpLoad, typeU32, lane, input); + if (storeThroughChain) { + const auto writePtr = module.AllocateId(); + module.AddFunction(accessOp, typePrivatePtr, writePtr, table, zero); + module.AddFunction(spv::OpStore, writePtr, lane); + } + module.AddFunction(spv::OpUGreaterThan, typeBool, rawCond, lane, zero); + module.AddFunction(spv::OpLogicalOr, typeBool, cond, rawCond, falseVal); + module.AddFunction(spv::OpIAdd, typeU32, baseIndex, one, one); + module.AddFunction(spv::OpSelect, typeU32, index, cond, baseIndex, baseIndex); + module.AddFunction(accessOp, typePrivatePtr, element, table, dynamicIndex ? lane : index); + module.AddFunction(spv::OpLoad, typeU32, loaded, element); + module.AddFunction(spv::OpIAdd, typeU32, sum, lane, loaded); + module.AddFunction(spv::OpStore, output, sum); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + return std::pair{ShaderRecompiler::SpecializeSpirv(module.Finalize()), thirty}; + }; + const auto [folded, expectedConstant] = build(false, false); + const auto foldedCounts = opcodeCounts(folded); + check(!foldedCounts.contains(accessOp), "a constant-index private composite table access chain was not removed"); + check(!foldedCounts.contains(spv::OpLogicalOr), "a right-false OpLogicalOr was not simplified"); + check(!foldedCounts.contains(spv::OpSelect), "an identical-arm OpSelect was not simplified"); + check(foldedCounts.contains(spv::OpLoad) && foldedCounts.at(spv::OpLoad) == 1u, "a constant-index private composite table load was not folded"); + bool addedThirty = false; + for (std::size_t cursor = 5; cursor < folded.size(); cursor += folded[cursor] >> 16u) { + if ((folded[cursor] >> 16u) == 0u) break; + if ((folded[cursor] & 0xffffu) == spv::OpIAdd && folded[cursor + 4u] == expectedConstant) addedThirty = true; + } + check(addedThirty, "a constant-index private composite table load did not fold to its element constant"); + + const auto [dynamic, unusedDynamic] = build(true, false); + static_cast(unusedDynamic); + const auto dynamicCounts = opcodeCounts(dynamic); + check(dynamicCounts.contains(accessOp) && dynamicCounts.at(accessOp) == 1u, "a dynamic-index private composite table access chain was removed"); + check(dynamicCounts.contains(spv::OpLoad) && dynamicCounts.at(spv::OpLoad) == 2u, "a dynamic-index private composite table load was folded"); + + const auto [mutated, unusedMutated] = build(false, true); + static_cast(unusedMutated); + const auto mutatedCounts = opcodeCounts(mutated); + check(mutatedCounts.contains(accessOp) && mutatedCounts.at(accessOp) == 2u, "a mutated private composite table access chain was removed"); + check(mutatedCounts.contains(spv::OpLoad) && mutatedCounts.at(spv::OpLoad) == 2u, "a mutated private composite table load was folded despite a store through an access chain"); + } +} + void testBlockBallotsOfOnePredicateShareOneBallot() { SpirvModule module; const auto typeBool = module.Type(spv::OpTypeBool); @@ -360,6 +440,7 @@ int main() { testFoldingLeavesNoDeadBlocksOrConstants(); testIdsBeyondTheBoundAreRejected(); testStructExtractsAreNotShuffled(); + testPrivateCompositeTableLoadFolding(); testRepeatedTypesShareOneId(); testOperandShapesDoNotCollide(); testOperandKindsAgree(); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index eb5a967d31..9534ce7716 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -936,6 +936,89 @@ OpStore %slot2 %second check(opcodeCount(acrossCall, spv::OpGroupNonUniformBallot) == 2, "a ballot was shared across a function call"); } +std::string privateTableModuleSource(const std::string& body) { + return R"(OpCapability Shader +OpMemoryModel Logical GLSL450 +OpEntryPoint GLCompute %main "main" %index +OpExecutionMode %main LocalSize 8 1 1 +OpDecorate %index BuiltIn LocalInvocationIndex +%void = OpTypeVoid +%bool = OpTypeBool +%uint = OpTypeInt 32 0 +%false = OpConstantFalse %bool +%zero = OpConstant %uint 0 +%one = OpConstant %uint 1 +%three = OpConstant %uint 3 +%sixteen = OpConstant %uint 16 +%twenty = OpConstant %uint 20 +%thirty = OpConstant %uint 30 +%array = OpTypeArray %uint %three +%initial = OpConstantComposite %array %sixteen %twenty %thirty +%input = OpTypePointer Input %uint +%privateArray = OpTypePointer Private %array +%private = OpTypePointer Private %uint +%function = OpTypeFunction %void +%index = OpVariable %input Input +%table = OpVariable %privateArray Private %initial +%output = OpVariable %private Private +%main = OpFunction %void None %function +%entry = OpLabel +%lane = OpLoad %uint %index +)" + body + R"(OpReturn +OpFunctionEnd +)"; +} + +void testSpecializedPrivateCompositeTableLoad() { + for (const bool inBounds : {false, true}) { + const auto accessName = inBounds ? "OpInBoundsAccessChain" : "OpAccessChain"; + const auto accessOp = inBounds ? spv::OpInBoundsAccessChain : spv::OpAccessChain; + try { + const auto folded = assemble(privateTableModuleSource( + "%rawCond = OpUGreaterThan %bool %lane %zero\n" + "%cond = OpLogicalOr %bool %rawCond %false\n" + "%baseOffset = OpIAdd %uint %one %one\n" + "%offset = OpSelect %uint %cond %baseOffset %baseOffset\n" + "%element = " + std::string(accessName) + " %private %table %offset\n" + "%loaded = OpLoad %uint %element\n" + "%sum = OpIAdd %uint %lane %loaded\n" + "OpStore %output %sum\n")); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(folded, Vulkan11, Spirv13, false, false)); + const auto specializedFolded = ShaderRecompiler::SpecializeSpirv(folded); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedFolded, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedFolded, accessOp) == 0u, std::string(accessName) + ": constant-index private table access chain was not removed"); + check(opcodeCount(specializedFolded, spv::OpLogicalOr) == 0u, std::string(accessName) + ": right-false OpLogicalOr was not simplified"); + check(opcodeCount(specializedFolded, spv::OpSelect) == 0u, std::string(accessName) + ": identical-arm OpSelect was not simplified"); + check(opcodeCount(specializedFolded, spv::OpLoad) == 1u, std::string(accessName) + ": constant-index private table load was not folded"); + check(storedLaneFunction(specializedFolded) == std::optional>({1u, 30u}), std::string(accessName) + ": constant-index private table load folded to the wrong value"); + + const auto dynamic = assemble(privateTableModuleSource( + "%element = " + std::string(accessName) + " %private %table %lane\n" + "%loaded = OpLoad %uint %element\n" + "OpStore %output %loaded\n")); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(dynamic, Vulkan11, Spirv13, false, false)); + const auto specializedDynamic = ShaderRecompiler::SpecializeSpirv(dynamic); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedDynamic, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedDynamic, accessOp) == 1u, std::string(accessName) + ": dynamic-index private table access chain was removed"); + check(opcodeCount(specializedDynamic, spv::OpLoad) == 2u, std::string(accessName) + ": dynamic-index private table load was folded"); + + const auto mutated = assemble(privateTableModuleSource( + "%writeElement = " + std::string(accessName) + " %private %table %zero\n" + "OpStore %writeElement %lane\n" + "%readElement = " + std::string(accessName) + " %private %table %one\n" + "%loaded = OpLoad %uint %readElement\n" + "OpStore %output %loaded\n")); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(mutated, Vulkan11, Spirv13, false, false)); + const auto specializedMutated = ShaderRecompiler::SpecializeSpirv(mutated); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedMutated, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedMutated, accessOp) == 2u, std::string(accessName) + ": mutated private table access chain was removed"); + check(opcodeCount(specializedMutated, spv::OpLoad) == 2u, std::string(accessName) + ": mutated private table load was folded despite a store through an access chain"); + } catch (const std::exception& error) { + check(false, std::string(accessName) + " private table specialization: " + error.what()); + } + } +} + } int main() { @@ -953,6 +1036,7 @@ int main() { testFoldedConstantUsedByUndecodedInstructionIsKept(); testSharedConstantsAreNotCopiedForUndecodedUsers(); testSharedBooleanConstantsKeepTheirValues(); + testSpecializedPrivateCompositeTableLoad(); testBallotsMergedIntoOneBlockShareOneBallot(optimizationEnabled); if (failures != 0) { std::fprintf(stderr, "%d optimizer check(s) failed\n", failures); @@ -961,3 +1045,4 @@ int main() { std::puts("SPIR-V optimizer checks passed"); return 0; } + diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp index 7ba31cd239..5a93b53303 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp @@ -171,6 +171,94 @@ void Check() { } } +constexpr std::array SubwordWord3{0x31005facu, 0x31006facu, 0x3100bfacu, 0x3100cfacu, 0x31012facu}; + +constexpr const char* SubwordNames[14] = { + "buffer_load_format_x 8_UINT +1", + "buffer_load_format_x 8_UINT +2", + "buffer_load_format_x 8_UINT +3", + "buffer_load_format_x 8_SINT +1", + "buffer_load_format_x 8_SINT +2", + "buffer_load_format_x 8_SINT +3", + "buffer_load_format_x 16_UINT +1", + "buffer_load_format_x 16_UINT +2", + "buffer_load_format_x 16_SINT +1", + "buffer_load_format_x 16_SINT +2", + "buffer_load_format_xy 8_8_UINT +1 x", + "buffer_load_format_xy 8_8_UINT +1 y", + "buffer_load_format_xy 8_8_UINT +2 x", + "buffer_load_format_xy 8_8_UINT +2 y", +}; + +std::vector SubwordCode() { + std::vector code{0x34060087u, 0x34500085u}; + std::uint32_t vdata = 10u; + const auto emitLoad = [&](std::uint32_t op, std::uint32_t srsrc, std::uint32_t offset, std::uint32_t words) { + code.push_back(0xe0001000u | (op << 18u) | offset); + code.push_back(0x80000028u | (srsrc << 16u) | (vdata << 8u)); + vdata += words; + }; + for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 2u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 3u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(0u, 4u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(0u, 5u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(1u, 6u, offset, 2u); + code.push_back(0xbf8c3f70u); + for (std::uint32_t index = 0u; index < 14u; ++index) { + code.push_back(0xe0701000u | (index * 4u)); + code.push_back(0x80010003u | ((10u + index) << 8u)); + } + code.push_back(0xbf810000u); + return code; +} + +void RunSubwordOffsets(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + std::vector userData(8u + 4u * SubwordWord3.size(), 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u), 0x31016facu); + std::copy(output.begin(), output.end(), userData.begin() + 4); + for (std::size_t index = 0; index < SubwordWord3.size(); ++index) { + const auto descriptor = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u), SubwordWord3[index]); + std::copy(descriptor.begin(), descriptor.end(), userData.begin() + 8 + 4 * index); + } + static const auto program = SubwordCode(); + const std::span code(program); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void CheckSubwordOffsets() { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t word = Rows[tid][0]; + const auto u8 = [&](std::uint32_t offset) { return (word >> (offset * 8u)) & 0xffu; }; + const auto s8 = [&](std::uint32_t offset) { return static_cast(static_cast(static_cast(u8(offset)))); }; + const auto u16 = [&](std::uint32_t offset) { return (word >> (offset * 8u)) & 0xffffu; }; + const auto s16 = [&](std::uint32_t offset) { return static_cast(static_cast(static_cast(u16(offset)))); }; + const std::array expected{ + u8(1u), u8(2u), u8(3u), + s8(1u), s8(2u), s8(3u), + u16(1u), u16(2u), + s16(1u), s16(2u), + u8(1u), u8(2u), + u8(2u), u8(3u), + }; + for (std::uint32_t index = 0; index < expected.size(); ++index) { + Expect(tid, Output[tid * Results + index], expected[index], SubwordNames[index]); + } + } +} + } int main() { @@ -179,6 +267,8 @@ int main() { if (!device) return VulkanTestSkipped; Run(*device); Check(); + RunSubwordOffsets(*device); + CheckSubwordOffsets(); std::puts("buffer format tests passed"); return 0; } catch (const std::exception& error) { @@ -186,3 +276,4 @@ int main() { return 1; } } + diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index a2ef8729d9..569298ea62 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -615,11 +615,16 @@ public: continue; } if (op == spv::OpLoad && instruction.size() == 4u && tablePointers.contains(instruction[3])) continue; - if (op == spv::OpName || op == spv::OpDecorate || op == spv::OpVariable || op == spv::OpEntryPoint) continue; - for (std::size_t index = 1; index < instruction.size(); ++index) { + if (op == spv::OpVariable || op == spv::OpEntryPoint) continue; + std::uint32_t switchLiteralWords = 1u; + if (op == spv::OpSwitch && instruction.size() > 1u) { + const auto* selectorType = resultTypes.find(instruction[1]); + if (selectorType != nullptr && types.contains(*selectorType) && types.at(*selectorType).width == 64u) switchLiteralWords = 2u; + } + VisitIdOperands(instruction, switchLiteralWords, [&](std::size_t index) { privateTables.erase(instruction[index]); if (const auto* table = tablePointers.find(instruction[index])) privateTables.erase(*table); - } + }); } } From 34a729529a64b52ded9eadc8023030fde8d61cec Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 16:40:09 +1300 Subject: [PATCH 085/318] fix(libc): keep the sign of a failed strtof and drop hex ERANGE like the console - strtof, strtold, wcstof and wcstold return -0.0 when nothing converts after a minus sign, unless the next character starts an inf or nan spelling; strtod, wcstod and atof keep +0.0. - strtof, strtold, wcstof and wcstold leave errno alone for hexadecimal text that overflows or underflows. - strtod and wcstod set ERANGE for hexadecimal text whose value lies below the smallest subnormal and rounds up to it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/src/Strings.cpp | 20 +++++++++++++++- core/libs/tests/GuestMath.cpp | 34 ++++++++++++++++++++++------ core/libs/tests/GuestWideStrings.cpp | 4 ++++ docs/dev/debt/libc.md | 1 + 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 65d6dfb9c6..80c513fbc0 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -187,7 +187,25 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha const TFloat value = parse(text, &parsedEnd); if (errno == ERANGE && value != 0 && (value < 0 ? -value : value) < std::numeric_limits::min()) errno = saved; if (end != nullptr) *end = parsedEnd; - return parsedEnd == text ? TFloat{0} : value; + const char* subject = text; + while (std::isspace(static_cast(*subject))) ++subject; + if (parsedEnd == text) { + const bool special = std::strchr("iInN", subject[1]) != nullptr && subject[1] != '\0'; + return !std::is_same_v && subject[0] == '-' && !special ? -TFloat{0} : TFloat{0}; + } + if (*subject == '+' || *subject == '-') ++subject; + const bool hexadecimal = subject[0] == '0' && (subject[1] == 'x' || subject[1] == 'X') && parsedEnd > subject + 2; + if (!hexadecimal) return value; + if constexpr (std::is_same_v) { + constexpr double smallest = std::numeric_limits::denorm_min(); + if ((value < 0 ? -value : value) == smallest) { + const long double exact = std::strtold(text, nullptr); + if ((exact < 0 ? -exact : exact) < smallest) errno = ERANGE; + } + } else if (errno == ERANGE) { + errno = saved; + } + return value; } } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index b027130f96..c848552aa2 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -494,6 +494,19 @@ static void CheckFloatUnderflowErrno() { Require(strtod_nid_postfix("1e400", &end) == HUGE_VAL && errno == ERANGE); errno = 0; Require(strtod_nid_postfix("2.2250738585072014e-308", &end) == 0x1p-1022 && errno == 0); + Require(strtod_nid_postfix("0x1.8p-1075", &end) == 0x1p-1074 && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("-0x1.000001p-1075", &end) == -0x1p-1074 && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("0x1p-1074", &end) == 0x1p-1074 && strtod_nid_postfix("0x1.8p-1074", &end) == 0x1p-1073 && errno == 0); + Require(strtod_nid_postfix("0x1p-1076", &end) == 0.0 && errno == ERANGE); + errno = 0; + Require(strtod_nid_postfix("0x1p+1100", &end) == HUGE_VAL && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("0x1p-150", &end) == 0.0f && strtof_nid_postfix("0x1p+200", &end) == HUGE_VALF && errno == 7); + Require(strtold_nid_postfix("0x1p-16446", &end) == 0.0L && strtold_nid_postfix("0x1p+20000", &end) == HUGE_VALL && errno == 7); + Require(strtof_nid_postfix("1e39", &end) == HUGE_VALF && errno == ERANGE); + errno = 0; } static void CheckHypotSpecialValue() { @@ -1206,16 +1219,23 @@ static void CheckLongDoubleRounding(long double value, std::int64_t expected, bo } } -static void CheckNoConversionIsPositiveZero() { +static void CheckNoConversionZero() { for (const char* text : {"", " ", "-", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { char* end = nullptr; const double parsed = strtod_nid_postfix(text, &end); Require(parsed == 0. && !std::signbit(parsed) && end == text); + const bool negative = std::strchr(text, '-') != nullptr; + const float parsedFloat = strtof_nid_postfix(text, &end); + Require(parsedFloat == 0.f && std::signbit(parsedFloat) == negative && end == text); + const long double parsedLong = strtold_nid_postfix(text, &end); + Require(parsedLong == 0.L && std::signbit(parsedLong) == negative && end == text); + Require(!std::signbit(atof_nid_postfix(text))); + } + for (const char* text : {"-i", "-in", "-N", "-nax"}) { + char* end = nullptr; const float parsedFloat = strtof_nid_postfix(text, &end); Require(parsedFloat == 0.f && !std::signbit(parsedFloat) && end == text); - const long double parsedLong = strtold_nid_postfix(text, &end); - Require(parsedLong == 0.L && !std::signbit(parsedLong) && end == text); - Require(!std::signbit(atof_nid_postfix(text))); + Require(!std::signbit(strtold_nid_postfix(text, nullptr))); } char* end = nullptr; const char negativeZero[] = "-0.0x"; @@ -1241,9 +1261,9 @@ static void CheckNoConversionIsPositiveZero() { const double parsedWide = wcstod_nid_postfix(wide, &wideEnd); Require(parsedWide == 0. && !std::signbit(parsedWide) && wideEnd == wide); const float parsedWideFloat = wcstof_nid_postfix(wide, &wideEnd); - Require(parsedWideFloat == 0.f && !std::signbit(parsedWideFloat) && wideEnd == wide); + Require(parsedWideFloat == 0.f && std::signbit(parsedWideFloat) && wideEnd == wide); const long double parsedWideLong = wcstold_nid_postfix(wide, &wideEnd); - Require(parsedWideLong == 0.L && !std::signbit(parsedWideLong) && wideEnd == wide); + Require(parsedWideLong == 0.L && std::signbit(parsedWideLong) && wideEnd == wide); } template @@ -1275,7 +1295,7 @@ static void CheckDinkumwareFloatCharacteristics() { int main() { CheckFloatClassification(); CheckDinkumwareConstants(); - CheckNoConversionIsPositiveZero(); + CheckNoConversionZero(); CheckMissingClassificationExports(); CheckFloatMathExports(); CheckIntegerConversions(); diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 809d47fe3e..b92c7b9d9f 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -319,6 +319,10 @@ int main() { require(wcstod_nid_postfix(scientific, &end) == 350.0 && end == scientific + 5); require(wcstod_nid_postfix(letters, &end) == 0.0 && end == letters); require(wcstof_nid_postfix(u"0.25", nullptr) == 0.25f); + const char16_t* sign = u" -x"; + require(std::signbit(wcstof_nid_postfix(sign, &end)) && end == sign && std::signbit(wcstold_nid_postfix(sign, &end)) && !std::signbit(wcstod_nid_postfix(sign, &end))); + *__error_nid_postfix() = 0; + require(wcstof_nid_postfix(u"0x1p+200", nullptr) == HUGE_VALF && *__error_nid_postfix() == 0); const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 6f00311858..b4625e5bb4 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -74,6 +74,7 @@ - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there +- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. From 810054bdf19a8000a95e4168285b3bab968f0c7c Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 16:40:55 +1300 Subject: [PATCH 086/318] docs(libc): move the strtof rounding entry clear of open edits Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index b4625e5bb4..30f0df3026 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -74,8 +74,8 @@ - [Wide-character conversions](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there -- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) +- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. From 0b52caf71887b2ac2c8a8aa605b49b43e7d5ae54 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 16:42:08 +1300 Subject: [PATCH 087/318] docs(libc): place the strtof rounding entry beside memmem Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 30f0df3026..8f46a9a874 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -58,6 +58,7 @@ - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. +- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. @@ -75,7 +76,6 @@ - [realpath](../../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) -- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. From 2d3ed2c8bf32b66f7571e7b5f4ed73e2dca0af6a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:39:45 +0000 Subject: [PATCH 088/318] feat(agc): count a registered shader's prepared entries Driver::PreparedEntries returns how many artifacts the registered header of a shader holds prepared, after its pending registration preparation settles, so tests can tell a prepared shader from one left to its first use. --- .../Execution/include/Driver/Driver.hpp | 1 + .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 3fb9f473b3..2d647d4f62 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -67,6 +67,7 @@ public: void ResolveGraphicsStagesAbi(std::span stages, std::span context, std::span primitive); void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); void SettleShaderPreparations(std::span stages); + [[nodiscard]] std::size_t PreparedEntries(const Shader* shader); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); static std::optional ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect); static void tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords, std::span> supplied = {}); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index b69bccd8fe..bef29257b4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1574,6 +1574,19 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std transaction.Commit(); } +std::size_t Driver::PreparedEntries(const Shader* shader) { + std::shared_ptr snapshot; + { + std::lock_guard lock(mutex); + if (shaders == nullptr) return 0; + snapshot = RegisteredHeader(*shaders, shader); + } + if (snapshot == nullptr) return 0; + std::unique_lock lock(snapshot->prepared->mutex); + AwaitRegisteredPreparation(*snapshot->prepared, lock); + return snapshot->prepared->entries.size(); +} + void Driver::ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive) { BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); From 3253ab49bb34542f41d46f4dd689065836a866b9 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:55:30 +0000 Subject: [PATCH 089/318] perf(agc): leave a lazily registered shader's resolved ABI to its first use Under APS5_LAZY_SHADER_PREPARE=1 registration prepares nothing, but ResolveShaderAbi still planned and prepared every compute or pixel stage whose ABI the title resolved, so a title that resolves every shader's ABI prepared them all at boot whether they ever drew. With lazy preparation ResolveShaderAbi now plans the stage with the ABI's state (its register and entry-point checks, as lazy registration keeps them), records the ABI key, and queues the ABI's context and primitive registers on the shader. The shader's first draw or dispatch plans and prepares the queued ABIs with that state before it looks for its artifact, so a shader that never draws or dispatches stays unprepared and one that does gets the entries eager resolution would have made. A stage whose plan fails is deferred as before. Without the switch nothing changes; graphics-stage ABIs are still prepared when they are resolved. Test: agc_driver_lazy_resolved_abi resolves the ABI of four lazily registered shaders and draws two. None holds prepared entries after resolution, the two that never drew still hold none, and a drawn one draws the same bytes as with eager preparation in a child process. --- .../include/Driver/Shaders/ShaderRegistry.hpp | 7 + .../src/Driver/Shaders/ShaderRegistry.cpp | 78 ++- .../tests/execution/LazyResolvedAbi.cpp | 455 ++++++++++++++++++ docs/dev/ARCHITECTURE.md | 2 +- 4 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 203940d7c3..07718b5ad6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -153,6 +153,13 @@ struct PreparedShaderState { bool deferred = false; }; struct PreparedShaders : PreparedShaderState { + struct DeferredAbi { + std::vector key; + std::vector> context; + std::vector> primitive; + std::weak_ptr device; + }; + std::vector deferredAbis; std::mutex mutex; std::condition_variable settled; std::exception_ptr failure; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index bef29257b4..c56915a0dd 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -591,6 +591,13 @@ void PublishRegisteredShader(std::shared_ptr& registry, const st namespace { +bool LazyShaderPreparation() { + static const bool lazy = std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr; + return lazy; +} + +void PrepareDeferredAbis(const ShaderSnapshot& snapshot); + void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler::RecompileRequest& request) { if (request.context.convertedSamples) { APS5_LOG_OUT_DEBUG("Shader 0x%llx samples a converted image; preparing its converted-sample variant at use", static_cast(request.shader.codeAddress)); @@ -626,6 +633,11 @@ std::shared_ptr SourceHandleFor(const Shad ShaderRecompiler::BuildPreparedShaderKey(request, key); std::unique_lock lock(snapshot.prepared->mutex); AwaitRegisteredPreparation(*snapshot.prepared, lock); + if (!snapshot.prepared->deferredAbis.empty()) { + lock.unlock(); + PrepareDeferredAbis(snapshot); + lock.lock(); + } for (const auto& entry : snapshot.prepared->entries) { if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) return entry.handle; } @@ -779,6 +791,11 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s struct PreparedKeyStorage {}; auto& key = HostThreadLocal, PreparedKeyStorage>(); ShaderRecompiler::BuildPreparedShaderKey(source, key); + if (!snapshot.prepared->deferredAbis.empty()) { + lock.unlock(); + PrepareDeferredAbis(snapshot); + lock.lock(); + } for (const auto& entry : snapshot.prepared->entries) { if (entry.codeOffset != codeOffset) continue; invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle); @@ -1071,6 +1088,39 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap return plan != nullptr ? PrepareRegistered(*plan) : std::vector{}; } +void PrepareDeferredAbis(const ShaderSnapshot& snapshot) { + std::vector deferred; + { + std::lock_guard lock(snapshot.prepared->mutex); + deferred.swap(snapshot.prepared->deferredAbis); + } + for (const auto& abi : deferred) { + const auto device = abi.device.lock(); + if (device == nullptr || snapshot.registeredState == nullptr) continue; + QueueState state{}; + state.shader = snapshot.registeredState->shader; + state.context = snapshot.registeredState->context; + state.userConfig = snapshot.registeredState->userConfig; + for (const auto& [offset, value] : abi.context) state.context[offset] = value; + for (const auto& [offset, value] : abi.primitive) state.userConfig[offset] = value; + std::vector entries; + try { + const auto plan = PlanRegistered(snapshot, *device, state, false); + if (plan == nullptr) continue; + plan->prebuildDevice = device; + entries = PrepareRegistered(*plan); + } catch (const std::exception& error) { + APS5_LOG_ERR("Shader 0x%llx could not be prepared at its first use for its resolved ABI (%s); preparing it for the use instead", static_cast(snapshot.codeAddress), error.what()); + continue; + } + std::lock_guard lock(snapshot.prepared->mutex); + for (auto& entry : entries) { + const auto duplicate = std::ranges::any_of(snapshot.prepared->entries, [&](const auto& existing) { return existing.codeOffset == entry.codeOffset && existing.handle->artifact == entry.handle->artifact; }); + if (!duplicate) snapshot.prepared->entries.push_back(std::move(entry)); + } + } +} + void DeferPipelineStage(PreparedShaderState& prepared, const ShaderSnapshot& snapshot, const char* reason) { if (!prepared.deferred) APS5_LOG_ERR("Shader 0x%llx could not be prepared when the title created its pipeline (%s); preparing it when it is used", static_cast(snapshot.codeAddress), reason); prepared.deferred = true; @@ -1624,6 +1674,31 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span, RegisteredAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); + if (LazyShaderPreparation()) { + const auto& resolved = transaction.Read(*snapshot).registeredAbis; + if (std::ranges::find(resolved, key) != resolved.end()) { + transaction.Commit(); + return; + } + std::optional invalid; + try { + static_cast(PlanRegistered(*snapshot, *localDevice, state, false)); + } catch (const std::exception& error) { + invalid = error.what(); + } + auto& prepared = transaction.Edit(*snapshot); + if (invalid) DeferPipelineStage(prepared, *snapshot, invalid->c_str()); + prepared.registeredAbis.push_back(key); + prepared.deferred = true; + transaction.Commit(); + if (invalid) return; + PreparedShaders::DeferredAbi deferred{key, {}, {}, localDevice}; + for (const auto reg : context) deferred.context.emplace_back(reg.offset, reg.value); + for (const auto reg : primitive) deferred.primitive.emplace_back(reg.offset, reg.value); + std::lock_guard lock(snapshot->prepared->mutex); + snapshot->prepared->deferredAbis.push_back(std::move(deferred)); + return; + } std::optional deferral; const auto prepare = [&] { try { @@ -1864,8 +1939,7 @@ void Driver::RegisterShader(const Shader* shader) { registered.context = snapshot.registeredState->context; registered.userConfig = snapshot.registeredState->userConfig; std::shared_ptr preparation; - static const bool lazy = std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr; - if (lazy) { + if (LazyShaderPreparation()) { static_cast(PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred)); snapshot.prepared->deferred = true; } else if (AsyncRegistrationPrepare()) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp new file mode 100644 index 0000000000..c48494d6ea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp @@ -0,0 +1,455 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Textures = 16; +constexpr std::uint32_t VertexBuffers = 3; +constexpr std::uint32_t MaxDraws = 2048; +constexpr std::uint32_t Format8888UNorm = 56; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t ClampEdge = 2; +constexpr std::uint32_t TexelByte = 4; +constexpr std::size_t SrtOffset = 0; +constexpr std::size_t SamplerOffset = 0x200; +constexpr std::size_t FrameConstantsOffset = 0x210; +constexpr std::size_t VertexTableOffset = 0x300; +constexpr std::size_t FrameDataOffset = 0x400; +constexpr std::size_t VertexDataOffset = 0x800; +constexpr std::size_t TextureOffset = 0x1000; +constexpr std::size_t TextureBytes = 4096; +constexpr std::size_t ObjectOffset = TextureOffset + Textures * TextureBytes; +constexpr std::size_t ObjectBytes = 256; +constexpr std::size_t ColorOffset = (ObjectOffset + MaxDraws * ObjectBytes + 65535) & ~static_cast(65535); +constexpr std::size_t ColorBytes = 65536; +constexpr std::uint32_t RenderTargetTileMode = 0x1b; +constexpr std::size_t BlockBytes = (ColorOffset + ColorBytes + 65535) & ~static_cast(65535); + +alignas(256) constexpr std::array VertexCode{ + 0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u, + 0x7e060280u, 0x7e0802f2u, + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, + 0xbf8cc07fu, + 0xe00c2000u, 0x80010a05u, 0xe00c2000u, 0x80020e05u, 0xe00c2000u, 0x80031205u, + 0xbf8c3f70u, + 0x0602030au, 0x0604050bu, 0x0606070cu, 0x0608090du, + 0x0602030eu, 0x0604050fu, 0x06060710u, 0x06080911u, + 0x06020312u, 0x06040513u, 0x06060714u, 0x06080915u, + 0xf80008cfu, 0x04030201u, + 0xbf810000u, 0xbf810000u, +}; + +constexpr std::size_t PixelWords = 10 + Textures * 10 + 5 + 8 + 3; + +constexpr std::array MakePixelCode() { + std::array code{}; + std::size_t n = 0; + for (const auto word : {0xf4080200u, 0xfa000200u, 0xf4080300u, 0xfa000210u, 0x7e0002f0u, 0x7e0202f0u, 0x7e100280u, 0x7e120280u, 0x7e140280u, 0x7e160280u}) code[n++] = word; + for (std::uint32_t texture = 0; texture < Textures; ++texture) { + for (const auto word : {0xf40c0400u, 0xfa000000u | (texture * 32u), 0xbf8cc07fu, 0xf09c0f08u, 0x00440400u, 0xbf8c3f70u, 0x06101104u, 0x06121305u, 0x06141506u, 0x06161707u}) code[n++] = word; + } + for (const auto word : {0xf4200606u, 0xfa000000u, 0xf4200642u, 0xfa000000u, 0xbf8cc07fu}) code[n++] = word; + for (const auto word : {0x10101018u, 0x10121218u, 0x10141418u, 0x10161618u, 0x10101019u, 0x10121219u, 0x10141419u, 0x10161619u}) code[n++] = word; + for (const auto word : {0xf800180fu, 0x0b0a0908u, 0xbf810000u}) code[n++] = word; + return code; +} + +alignas(256) constexpr std::array PixelCode = MakePixelCode(); + +struct Header { + Shader shader{}; + ShaderUserData userData{}; +}; + +alignas(8) Header VertexHeader{}; +alignas(8) Header PixelHeader{}; + +void Register(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) { + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = static_cast(bytes); + header.shader.code = code; + header.shader.type = type; + header.shader.user_data = &header.userData; + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536)); + if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes); +#else + constexpr std::uintptr_t alignment = 65536; + void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped != MAP_FAILED) { + const auto begin = reinterpret_cast(mapped); + const auto aligned = (begin + alignment - 1) & ~(alignment - 1); + if (aligned != begin) munmap(mapped, aligned - begin); + if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast(aligned + BlockBytes), begin + alignment - aligned); + block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes); + } +#endif + Require(block != nullptr, "lazy resolved ABI: cannot allocate the guest block"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + + std::uint64_t At(std::size_t offset) const { return static_cast(reinterpret_cast(block + offset)); } + std::uint8_t* Bytes(std::size_t offset) const { return block + offset; } + +private: + std::uint8_t* block = nullptr; +}; + +struct Layout { + const GuestBlock& data; + const GuestBlock& textures; + const GuestBlock& target; +}; + +std::array BufferDescriptor(std::uint64_t address, std::uint32_t stride, std::uint32_t records, std::uint32_t word3) { + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, word3}; +} + +void Prepare(const Layout& layout) { + const auto& block = layout.data; + for (std::uint32_t texture = 0; texture < Textures; ++texture) { + const auto address = layout.textures.At(TextureOffset + texture * TextureBytes); + std::memset(layout.textures.Bytes(TextureOffset + texture * TextureBytes), static_cast(TexelByte), TextureBytes); + const std::array words{static_cast(address >> 8u), static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (3u << 30u), 7u << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u}; + std::memcpy(block.Bytes(SrtOffset + texture * 32u), words.data(), sizeof(words)); + } + const std::array sampler{ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u}; + std::memcpy(block.Bytes(SamplerOffset), sampler.data(), sizeof(sampler)); + const auto frame = BufferDescriptor(block.At(FrameDataOffset), 0, 256, 0x31016facu); + std::memcpy(block.Bytes(FrameConstantsOffset), frame.data(), sizeof(frame)); + const float one = 1.0f; + std::memcpy(block.Bytes(FrameDataOffset), &one, sizeof(one)); + for (std::uint32_t buffer = 0; buffer < VertexBuffers; ++buffer) { + const auto vertices = BufferDescriptor(block.At(VertexDataOffset + buffer * 0x100u), 16, 3, 0x0104dfacu); + std::memcpy(block.Bytes(VertexTableOffset + buffer * 16u), vertices.data(), sizeof(vertices)); + } + for (std::uint32_t draw = 0; draw < MaxDraws; ++draw) std::memcpy(block.Bytes(ObjectOffset + draw * ObjectBytes), &one, sizeof(one)); +} + +void Append(std::vector& commands, std::uint32_t opcode, std::initializer_list payload) { + commands.push_back(0xc0000000u | (static_cast(payload.size() - 1) << 16u) | (opcode << 8u)); + commands.insert(commands.end(), payload); +} + +std::vector Commands(const Layout& layout, std::uint32_t draws, std::uintptr_t pixel = reinterpret_cast(PixelCode.data()), std::uint32_t blend = 0) { + const auto& block = layout.data; + const auto color = layout.target.At(ColorOffset); + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto srt = block.At(SrtOffset); + const auto table = block.At(VertexTableOffset); + const float scale = 32.0f; + const std::vector> context{ + {0x2d5, 0x2000}, + {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, + {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, + {0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa}, + {0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff}, + {0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240}, + {0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010}, + {0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028}, + {0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u}, + {0x3b8, 0x9000000u | (RenderTargetTileMode << 14u)}, {0x1e0, blend}, + {0xc, 0}, {0xd, 0x40040}, + {0x81, 0x80000000}, {0x82, 0x40040}, + {0x90, 0x80000000}, {0x91, 0x40040}, + {0x94, 0x80000000}, {0x95, 0x40040}, + {0x318, static_cast(color >> 8u)}, {0x390, static_cast(color >> 40u)}, + {0x10f, std::bit_cast(scale)}, {0x110, std::bit_cast(scale)}, + {0x111, std::bit_cast(-2.0f)}, {0x112, std::bit_cast(2.0f)}, + {0x113, std::bit_cast(1.0f)}, {0x114, 0}, + {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, + {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, + }; + const std::vector> shader{ + {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 2u << 1u}, + {0x8c, static_cast(table)}, {0x8d, static_cast(table >> 32u)}, + {0x008, static_cast(pixel >> 8u)}, {0x009, static_cast(pixel >> 40u)}, {0x00b, 8u << 1u}, + {0x00c, static_cast(srt)}, {0x00d, static_cast(srt >> 32u)}, {0x00e, 0}, {0x00f, 0}, + }; + std::vector commands; + Append(commands, 0x79, {0x242, 4}); + for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); + for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); + for (std::uint32_t draw = 0; draw < draws; ++draw) { + const auto object = BufferDescriptor(block.At(ObjectOffset + draw * ObjectBytes), 0, 16, 0x31016facu); + Append(commands, 0x76, {0x010, object[0], object[1], object[2], object[3]}); + Append(commands, 0x2d, {3, 2}); + } + return commands; +} + +double Submit(std::vector& commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + const auto start = std::chrono::steady_clock::now(); + Require(sceAgcDriverSubmitDcb(&packet) == 0, "lazy resolved ABI: the draw submission failed"); + AgcDriverWaitIdle_nid_postfix(); + return std::chrono::duration(std::chrono::steady_clock::now() - start).count(); +} + +unsigned ColorByte(const GuestBlock& block) { + AgcDriver::GuestMemory::FlushGpuWrites(static_cast(block.At(ColorOffset)), ColorBytes); + unsigned brightest = 0; + for (std::size_t i = 0; i < ColorBytes; ++i) brightest = std::max(brightest, static_cast(block.Bytes(ColorOffset)[i])); + return brightest; +} + +void Check(const Layout& layout) { + std::memset(layout.target.Bytes(ColorOffset), 0, ColorBytes); + auto commands = Commands(layout, 16); + Submit(commands); + const auto byte = ColorByte(layout.target); + Require(byte == Textures * TexelByte, "lazy resolved ABI: 16 draws sampling 16 textures of byte " + std::to_string(TexelByte) + " wrote " + std::to_string(byte) + ", expected " + std::to_string(Textures * TexelByte)); +} + + +std::vector SyntheticPixelCode(std::uint32_t index, std::uint32_t blocks) { + std::vector code; + code.reserve(PixelCode.size() + 2 + blocks * 8); + code.push_back(0x7e1802ffu); + code.push_back(0x3f800000u + index); + for (const auto word : PixelCode) { + if (word == 0xf800180fu) { + for (std::uint32_t block = 0; block < blocks; ++block) { + for (const auto multiply : {0x10101018u, 0x10121218u, 0x10141418u, 0x10161618u, 0x10101019u, 0x10121219u, 0x10141419u, 0x10161619u}) code.push_back(multiply); + } + } + code.push_back(word); + } + return code; +} + +struct SyntheticHeader { + Shader shader{}; + ShaderUserData userData{}; + std::array registers{}; + std::array context{{{0x1b6, 0}}}; +}; + +struct Synthetic { + SyntheticHeader header; + std::uint32_t* code = nullptr; +}; + +class ProgramArena { +public: + explicit ProgramArena(std::size_t size) : bytes((size + 65535u) & ~std::size_t{65535}) { +#ifdef _WIN32 + base = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(bytes, 65536)); + if (base != nullptr) GuestArena::GuestArenaCommit_nid_postfix(base, bytes, PAGE_READWRITE, bytes); +#else + void* mapped = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + base = mapped != MAP_FAILED ? static_cast(mapped) : nullptr; +#endif + Require(base != nullptr, "lazy resolved ABI: cannot allocate the program arena"); + GuestAllocations::Mutation().Add(base, bytes, true, true, true); + } + ProgramArena(const ProgramArena&) = delete; + ProgramArena& operator=(const ProgramArena&) = delete; + std::uint32_t* Place(std::span code) { + const auto aligned = (used + 255u) & ~std::size_t{255}; + Require(aligned + code.size_bytes() <= bytes, "lazy resolved ABI: the program arena is full"); + auto* destination = reinterpret_cast(base + aligned); + std::memcpy(destination, code.data(), code.size_bytes()); + used = aligned + code.size_bytes(); + return destination; + } + +private: + std::size_t bytes; + std::size_t used = 0; + std::uint8_t* base = nullptr; +}; + +void RegisterSynthetic(Synthetic& program, ProgramArena& arena, const std::vector& code) { + program.code = arena.Place(code); + const auto address = reinterpret_cast(program.code); + auto& header = program.header; + header.registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00b, 8u << 1u}, {0x00a, 0}}}; + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(SyntheticHeader); + header.shader.shader_size = static_cast(code.size() * sizeof(std::uint32_t)); + header.shader.code = program.code; + header.shader.type = 1; + header.shader.user_data = &header.userData; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = static_cast(header.registers.size()); + header.shader.cx_registers = header.context.data(); + header.shader.num_cx_registers = static_cast(header.context.size()); + AgcDriverRegisterShader_nid_postfix(&header.shader); +} + +constexpr std::uint32_t Programs = 4; +constexpr std::uint32_t Drawn = 2; + +void SetEnvironment(const char* name, const std::string& value) { +#ifdef _WIN32 + _putenv_s(name, value.c_str()); +#else + setenv(name, value.c_str(), 1); +#endif +} + +void Settle(std::span programs) { + std::vector registered; + for (const auto& program : programs) registered.push_back(&program.header.shader); + AgcDriverEndShaderPreparation_nid_postfix(AgcDriverBeginShaderPreparation_nid_postfix(registered.data(), registered.size())); + AgcDriverWaitIdle_nid_postfix(); +} + +std::vector Draw(const Layout& layout, const Synthetic& program) { + std::memset(layout.target.Bytes(ColorOffset), 0, ColorBytes); + auto commands = Commands(layout, 1, reinterpret_cast(program.code)); + Submit(commands); + AgcDriver::GuestMemory::FlushGpuWrites(static_cast(layout.target.At(ColorOffset)), ColorBytes); + return {layout.target.Bytes(ColorOffset), layout.target.Bytes(ColorOffset) + ColorBytes}; +} + +std::size_t Entries(const Synthetic& program) { + return AgcDriver::DriverDetail::Driver::Get().PreparedEntries(&program.header.shader); +} + +void EagerReference(const Layout& layout, const std::filesystem::path& output) { + ProgramArena arena(1u << 20u); + std::vector programs(1); + RegisterSynthetic(programs.front(), arena, SyntheticPixelCode(1, 0)); + AgcDriverResolveShaderAbi_nid_postfix(&programs.front().header.shader, {}, {}); + Settle(programs); + Require(Entries(programs.front()) != 0, "lazy resolved ABI: eager preparation left the resolved shader unprepared"); + const auto bytes = Draw(layout, programs.front()); + std::ofstream file(output, std::ios::binary | std::ios::trunc); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + Require(file.good(), "lazy resolved ABI: cannot write the eager reference"); +} + +void LazyResolution(const Layout& layout, const std::filesystem::path& reference) { + std::vector expected(ColorBytes); + { + std::ifstream file(reference, std::ios::binary); + file.read(reinterpret_cast(expected.data()), static_cast(expected.size())); + Require(file.gcount() == static_cast(expected.size()), "lazy resolved ABI: the eager reference is missing or short"); + } + ProgramArena arena(1u << 20u); + std::vector programs(Programs); + for (std::uint32_t index = 0; index < Programs; ++index) { + RegisterSynthetic(programs[index], arena, SyntheticPixelCode(index + 1, 0)); + AgcDriverResolveShaderAbi_nid_postfix(&programs[index].header.shader, {}, {}); + } + Settle(programs); + for (std::uint32_t index = 0; index < Programs; ++index) { + const auto entries = Entries(programs[index]); + Require(entries == 0, "lazy resolved ABI: ResolveShaderAbi prepared " + std::to_string(entries) + " entries for shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); + } + std::vector first; + for (std::uint32_t index = 0; index < Drawn; ++index) { + auto bytes = Draw(layout, programs[index]); + if (index == 0) first = std::move(bytes); + Require(Entries(programs[index]) != 0, "lazy resolved ABI: drawn shader " + std::to_string(index) + " holds no prepared entries"); + } + for (std::uint32_t index = Drawn; index < Programs; ++index) { + const auto entries = Entries(programs[index]); + Require(entries == 0, "lazy resolved ABI: shader " + std::to_string(index) + ", which never drew, holds " + std::to_string(entries) + " prepared entries"); + } + Require(std::ranges::any_of(first, [](std::uint8_t byte) { return byte != 0; }), "lazy resolved ABI: the drawn shader wrote nothing"); + Require(first == expected, "lazy resolved ABI: the shader prepared at its first draw drew other bytes than with eager preparation"); + std::printf("lazy resolved ABI: %u of %u ABI-resolved shaders drew; the %u that never drew hold no prepared entries, and a drawn one drew the same %zu bytes as with eager preparation\n", Drawn, Programs, Programs - Drawn, first.size()); +} + +} + +int main(int argc, char** argv) { + const bool eager = argc == 2 && std::string(argv[1]) == "--eager-reference"; + std::filesystem::path reference; + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + if (eager) { + const char* path = std::getenv("LAZY_RESOLVED_ABI_REFERENCE"); + Require(path != nullptr, "lazy resolved ABI: the eager child has no reference path"); + reference = path; + } else { + reference = std::filesystem::temp_directory_path() / ("aps5-lazy-resolved-abi-" + std::to_string(std::random_device{}()) + ".bin"); + SetEnvironment("LAZY_RESOLVED_ABI_REFERENCE", reference.string()); + const std::string command = "\"" + std::string(argv[0]) + "\" --eager-reference"; + Require(std::system(command.c_str()) == 0, "lazy resolved ABI: the eager reference process failed"); + SetEnvironment("APS5_LAZY_SHADER_PREPARE", "1"); + } + Register(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); + Register(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1); + GuestBlock data; + GuestBlock textures; + GuestBlock target; + const Layout layout{data, textures, target}; + Prepare(layout); + Check(layout); + if (eager) EagerReference(layout, reference); + else LazyResolution(layout, reference); + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (...) { + } + if (!eager && !reference.empty()) { + std::error_code ignored; + std::filesystem::remove(reference, ignored); + } + return 1; + } + if (!eager) { + std::error_code ignored; + std::filesystem::remove(reference, ignored); + } + return 0; +} diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 574244a6da..ced76416f2 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -120,7 +120,7 @@ flowchart LR - A dispatch's resource build runs stage A (binding plan, buffers, descriptor set) without the GPU lock and stage B (image lookups) under it. Between them `VulkanDevice::presync` waits, still without the lock, for recorded work that writes a surface stage B would read on the CPU and for recorded work that writes the DCC keys of an image the dispatch binds (`ShaderResources::PresyncSurfaces`), so the locked `WaitForKeyWriters` finds nothing pending to synchronize with. A key range is waited for only when recorded work writes it, the key-store memo does not give its value (`DccKeyWritersPending`, without the mutex) and the pending stores do not give it either (`DccKeyWriterNeedsSync`, under the mutex, the same test `WaitForKeyWriters` makes before it synchronizes). Stage A's own key scan runs before that wait, and stage B compares it with the keys it reads under the lock as before. `APS5_NO_DCC_PRESYNC=1` leaves the key ranges out; `APS5_NO_PRESYNC=1` disables the whole pre-sync. - The shader registry maps a code address to every header registered for that code, in registration order. Static, graphics and rectangle ABI resolution pick the header at the bound address (or one equal to it, `SameHeader`, which also takes a hull header fused by `sceAgcFuseShaderHalves` (type 3) as the registered back half (type 7) it copies, the fused registers aside; graphics ABI resolution then uses the fused header's shader registers); draws and dispatches pick the program by the stage's binary type (`RegisteredProgram`), and a front entry matching both type 2 and type 4 throws. Within a type the last registration wins; re-registering an identical earlier header makes its snapshot current again without preparing it again, and changed code drops the older headers. - 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. -- `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. Explicit ABI resolution still prepares its requested stages. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. +- `APS5_LAZY_SHADER_PREPARE=1` retains registration validation and decoding but discards the temporary plan without queueing preparation. The snapshot is deferred, not pending; draws and dispatches prepare and cache it on first use. A compute or pixel stage's explicit ABI resolution (`ResolveShaderAbi`) plans the stage with the ABI's state, records the ABI and leaves its preparation to the shader's first use, which prepares it with that state (`PreparedShaders::deferredAbis`), so a shader that never draws or dispatches stays unprepared; a graphics-stage ABI is still prepared when it is resolved. This opt-in mode takes precedence over synchronous registration preparation and trades background CPU/memory use for possible first-use stalls; disk caches and shader output are unchanged. - On devices with graphics pipeline libraries (`SpirvTarget::registrationModules`), a vertex or pixel artifact is emitted in registration form: the buffer V# words, image modes, swizzles and mip ranges, depth-compare state, export mappings and vertex component selectors it used to specialize on per draw are read at run time (image `ResourceMetadata` and a few state dwords in the shader data), descriptor heaps keep every element in place (an unused one gets a null or placeholder descriptor), and the pixel stage's bindings live in descriptor set 1 so each stage's library depends only on its own set layout (`INDEPENDENT_SETS`). An artifact above `registrationWordLimit` SPIR-V words is re-emitted specialized, and `CompiledShaderArtifact::registrationModule` records which form it has. For artifacts in registration form, `RegistrationResult` materializes the module once, and `PrebuildStageLibrary` builds its pre-rasterization or fragment-shader library on a separate one-thread pool (`APS5_PREBUILD_THREADS`) with a fixed default state, pairing it with a stub stage. A later draw with that state finds both libraries in the store and only links. - A draw or dispatch whose state matches no artifact prepared at registration (a title that builds its static ABI state itself, without `sceAgcDriverResolve*Abi`) gets one prepared at use by `SourceHandleFor` or `InvocationFor`, logged once per artifact and kept in the snapshot for the next use. - A stage that `sceAgcDriverResolve*Abi` cannot prepare for the state the title creates its pipeline with (a compute shader that calls user-data pointers, a descriptor the resource plan rejects) is left to its first use like a stage deferred at registration: `ResolveShaderAbi` and `PrepareGraphicsStages` catch the failure inside their own preparation transaction, so the `ShaderPreparationScope` the title's call runs in still commits, log it once per shader, mark the shader deferred and record the ABI key, so a repeated call prepares nothing; `ResolvePreparedGraphics` skips a deferred stage's rectangles until a draw has prepared it. A draw or dispatch that cannot prepare the stage either fails as before. Driver failures, `ProcessShutdown` and a failure stored by the shader's registration preparation still propagate from the ABI call. From 6e0066b5dbe12bd2134a22749622201b9bfb9691 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:36:22 +0000 Subject: [PATCH 090/318] test(agc): run the lazy resolved ABI test's eager reference without the switch agc_driver_lazy_resolved_abi spawns itself with --eager-reference to draw the reference bytes with eager preparation, and only sets APS5_LAZY_SHADER_PREPARE for itself after the child returns. Run with the switch already exported, as a CI run of the whole suite with lazy preparation does, the child inherited it, prepared nothing at its ABI resolution and failed with "eager preparation left the resolved shader unprepared". The test now removes the variable before it spawns the child. agc_driver_lazy_resolved_abi_switch_set runs it with the switch exported. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +++++ .../libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 21cbf9e3e8..10d7e03a0e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2175,6 +2175,11 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_lazy_resolved_abi_switch_set COMMAND agc_driver_lazy_resolved_abi_tests) + set_tests_properties(agc_driver_lazy_resolved_abi_switch_set PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_LAZY_SHADER_PREPARE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_lazy_resolved_abi_switch_set PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30) add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) set_tests_properties(agc_driver_multisample_readback PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp index c48494d6ea..1c1f0b21d8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp @@ -338,6 +338,14 @@ void SetEnvironment(const char* name, const std::string& value) { #endif } +void ClearEnvironment(const char* name) { +#ifdef _WIN32 + _putenv_s(name, ""); +#else + unsetenv(name); +#endif +} + void Settle(std::span programs) { std::vector registered; for (const auto& program : programs) registered.push_back(&program.header.shader); @@ -420,6 +428,7 @@ int main(int argc, char** argv) { } else { reference = std::filesystem::temp_directory_path() / ("aps5-lazy-resolved-abi-" + std::to_string(std::random_device{}()) + ".bin"); SetEnvironment("LAZY_RESOLVED_ABI_REFERENCE", reference.string()); + ClearEnvironment("APS5_LAZY_SHADER_PREPARE"); const std::string command = "\"" + std::string(argv[0]) + "\" --eager-reference"; Require(std::system(command.c_str()) == 0, "lazy resolved ABI: the eager reference process failed"); SetEnvironment("APS5_LAZY_SHADER_PREPARE", "1"); From 9f6bb2392cf0b4914b69f7afe899673a36a525df Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:43:12 +0000 Subject: [PATCH 091/318] test(agc): compare a lazily resolved ABI's entries, libraries and cache files with eager preparation agc_driver_lazy_resolved_abi compared only the bytes one lazily resolved shader drew with eager preparation's. Both processes now run the same scenario, and the lazy one compares what it ends with against the eager one's report: - five synthetic pixel shaders, four resolved with empty ABI registers and one with its draw's own context and primitive registers, three of them drawn; - each drawn shader's prepared entries, fingerprinted by the SPIR-V content hash and the source key, equal what eager resolution and the draw made: a draw with the ABI's state finds the ABI's entries, a draw with another state adds its own. Entries eager registration alone made for a state no ABI names are left out, as lazy registration makes none; - the shaders that never drew hold no entries, and nothing is prepared or prebuilt at resolution; - the pipeline libraries built, after the prebuild pool has drained, equal eager preparation's, so the first use prebuilds the registration module's library as resolution did; - both runs share one shader disk cache, and the lazy run writes no file the eager run did not. Driver::PreparedEntryHashes and Driver::PrebuiltLibraries expose the fingerprints and the library counts to the test. Each check fails when its part of the first-use preparation is removed: without the prebuild device the libraries differ (eager 1 2 2 1, lazy 1 1 1 1), and without the ABI's context registers the shader drawn with them holds other entries. --- .../Execution/include/Driver/Driver.hpp | 2 + .../src/Driver/Shaders/ShaderRegistry.cpp | 30 +++ .../tests/execution/LazyResolvedAbi.cpp | 210 ++++++++++++++---- 3 files changed, 200 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 2d647d4f62..ca5f882eb9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -68,6 +68,8 @@ public: void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); void SettleShaderPreparations(std::span stages); [[nodiscard]] std::size_t PreparedEntries(const Shader* shader); + [[nodiscard]] std::vector> PreparedEntryHashes(const Shader* shader); + [[nodiscard]] std::array PrebuiltLibraries(); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); static std::optional ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect); static void tagVariantRuns(DispatchVariant& variant, std::span regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords, std::span> supplied = {}); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index c56915a0dd..6cdad1b526 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -4,6 +4,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" +#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "CacheKey.hpp" @@ -1637,6 +1638,35 @@ std::size_t Driver::PreparedEntries(const Shader* shader) { return snapshot->prepared->entries.size(); } +std::vector> Driver::PreparedEntryHashes(const Shader* shader) { + std::shared_ptr snapshot; + { + std::lock_guard lock(mutex); + if (shaders == nullptr) return {}; + snapshot = RegisteredHeader(*shaders, shader); + } + if (snapshot == nullptr) return {}; + std::unique_lock lock(snapshot->prepared->mutex); + AwaitRegisteredPreparation(*snapshot->prepared, lock); + std::vector> hashes; + for (const auto& entry : snapshot->prepared->entries) { + if (entry.handle == nullptr || entry.handle->artifact == nullptr) continue; + const auto hash = entry.handle->artifact->artifact.spirv.ContentHash(); + std::uint64_t key = 0xcbf29ce484222325ull; + for (const auto word : entry.handle->staticKey) key = (key ^ word) * 0x100000001b3ull; + hashes.push_back({hash.low, hash.high ^ key}); + } + std::ranges::sort(hashes); + return hashes; +} + +std::array Driver::PrebuiltLibraries() { + if (auto* pool = RegistrationPreparePool::ExistingPrebuild()) pool->AwaitSubmitted(); + const auto localDevice = device.Load(); + if (localDevice == nullptr) return {}; + return Graphics::PipelineLibraryCountersOf(localDevice->Device()).built; +} + void Driver::ResolveShaderAbi(const Shader* shader, std::span context, std::span primitive) { BindTitleThread(); PerformanceContext timingContext(FrameTiming::Preparation()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp index 1c1f0b21d8..636deb9230 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include #include @@ -29,6 +31,8 @@ #include #include #include +#include +#include #include #include #include @@ -179,14 +183,12 @@ void Append(std::vector& commands, std::uint32_t opcode, std::ini commands.insert(commands.end(), payload); } -std::vector Commands(const Layout& layout, std::uint32_t draws, std::uintptr_t pixel = reinterpret_cast(PixelCode.data()), std::uint32_t blend = 0) { - const auto& block = layout.data; +constexpr std::pair PrimitiveType{0x242, 4}; + +std::vector> DrawContext(const Layout& layout, std::uint32_t blend = 0) { const auto color = layout.target.At(ColorOffset); - const auto vertex = reinterpret_cast(VertexCode.data()); - const auto srt = block.At(SrtOffset); - const auto table = block.At(VertexTableOffset); const float scale = 32.0f; - const std::vector> context{ + return { {0x2d5, 0x2000}, {0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800}, {0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0}, @@ -208,6 +210,14 @@ std::vector Commands(const Layout& layout, std::uint32_t draws, s {0xb4, 0}, {0xb5, std::bit_cast(1.0f)}, {0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0}, }; +} + +std::vector Commands(const Layout& layout, std::uint32_t draws, std::uintptr_t pixel = reinterpret_cast(PixelCode.data()), std::uint32_t blend = 0) { + const auto& block = layout.data; + const auto vertex = reinterpret_cast(VertexCode.data()); + const auto srt = block.At(SrtOffset); + const auto table = block.At(VertexTableOffset); + const auto context = DrawContext(layout, blend); const std::vector> shader{ {0xc8, static_cast(vertex >> 8u)}, {0xc9, static_cast(vertex >> 40u)}, {0x8b, 2u << 1u}, {0x8c, static_cast(table)}, {0x8d, static_cast(table >> 32u)}, @@ -215,7 +225,7 @@ std::vector Commands(const Layout& layout, std::uint32_t draws, s {0x00c, static_cast(srt)}, {0x00d, static_cast(srt >> 32u)}, {0x00e, 0}, {0x00f, 0}, }; std::vector commands; - Append(commands, 0x79, {0x242, 4}); + Append(commands, 0x79, {PrimitiveType.first, PrimitiveType.second}); for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value}); for (const auto& [offset, value] : shader) Append(commands, 0x76, {offset, value}); for (std::uint32_t draw = 0; draw < draws; ++draw) { @@ -365,50 +375,153 @@ std::size_t Entries(const Synthetic& program) { return AgcDriver::DriverDetail::Driver::Get().PreparedEntries(&program.header.shader); } -void EagerReference(const Layout& layout, const std::filesystem::path& output) { +constexpr std::uint32_t Matching = Programs; +constexpr std::array DrawnPrograms{0, 1, Matching}; + +using Report = std::map; + +std::string Text(std::span> hashes) { + std::string text = std::to_string(hashes.size()); + char word[40]; + for (const auto& hash : hashes) { + std::snprintf(word, sizeof(word), " %016llx%016llx", static_cast(hash[1]), static_cast(hash[0])); + text += word; + } + return text; +} + +std::string Text(const std::array& libraries) { + std::string text; + for (const auto count : libraries) text += (text.empty() ? "" : " ") + std::to_string(count); + return text; +} + +std::size_t Count(const std::string& text) { + return static_cast(std::stoull(text.substr(0, text.find(' ')))); +} + +std::vector Prints(const std::string& text) { + std::vector prints; + std::istringstream stream(text); + std::string word; + stream >> word; + while (stream >> word) prints.push_back(word); + std::ranges::sort(prints); + return prints; +} + +std::string Joined(std::span prints) { + std::string text = std::to_string(prints.size()); + for (const auto& print : prints) text += " " + print; + return text; +} + +std::vector EagerResolutionEntries(const Report& eager, std::uint32_t index) { + const auto id = std::to_string(index); + const auto registered = Prints(eager.at("registered " + id)); + const auto drawn = Prints(eager.at("drawn " + id)); + if (Count(eager.at("resolved " + id)) == registered.size()) return drawn; + std::vector made; + std::ranges::set_difference(drawn, registered, std::back_inserter(made)); + return made; +} + +Report Scenario(const Layout& layout, std::vector& first) { + auto& driver = AgcDriver::DriverDetail::Driver::Get(); + Report report; + report["libraries before"] = Text(driver.PrebuiltLibraries()); ProgramArena arena(1u << 20u); - std::vector programs(1); - RegisterSynthetic(programs.front(), arena, SyntheticPixelCode(1, 0)); - AgcDriverResolveShaderAbi_nid_postfix(&programs.front().header.shader, {}, {}); + std::vector programs(Matching + 1); + for (std::uint32_t index = 0; index <= Matching; ++index) RegisterSynthetic(programs[index], arena, SyntheticPixelCode(index + 1, 0)); Settle(programs); - Require(Entries(programs.front()) != 0, "lazy resolved ABI: eager preparation left the resolved shader unprepared"); - const auto bytes = Draw(layout, programs.front()); + for (std::uint32_t index = 0; index <= Matching; ++index) report["registered " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + for (std::uint32_t index = 0; index < Programs; ++index) AgcDriverResolveShaderAbi_nid_postfix(&programs[index].header.shader, {}, {}); + std::vector context; + for (const auto& [offset, value] : DrawContext(layout)) context.push_back({offset, value}); + const std::array primitive{{{PrimitiveType.first, PrimitiveType.second}}}; + AgcDriverResolveShaderAbi_nid_postfix(&programs[Matching].header.shader, context, primitive); + Settle(programs); + for (std::uint32_t index = 0; index <= Matching; ++index) report["resolved " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + report["libraries resolved"] = Text(driver.PrebuiltLibraries()); + for (const auto index : DrawnPrograms) { + auto bytes = Draw(layout, programs[index]); + Require(std::ranges::any_of(bytes, [](std::uint8_t byte) { return byte != 0; }), "lazy resolved ABI: drawn shader " + std::to_string(index) + " wrote nothing"); + if (index == 0) first = std::move(bytes); + } + for (std::uint32_t index = 0; index <= Matching; ++index) report["drawn " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + report["libraries drawn"] = Text(driver.PrebuiltLibraries()); + return report; +} + +std::filesystem::path ReportPath(const std::filesystem::path& reference) { + auto path = reference; + path += ".report"; + return path; +} + +std::set CacheFiles(const std::filesystem::path& cache) { + std::set files; + for (const auto& entry : std::filesystem::recursive_directory_iterator(cache)) { + if (entry.is_regular_file() && entry.path().filename().string().rfind("vk-pipelines-", 0) != 0) files.insert(std::filesystem::relative(entry.path(), cache).generic_string()); + } + return files; +} + +void EagerReference(const Layout& layout, const std::filesystem::path& output) { + std::vector bytes; + const auto report = Scenario(layout, bytes); + for (std::uint32_t index = 0; index <= Matching; ++index) Require(Count(report.at("resolved " + std::to_string(index))) != 0, "lazy resolved ABI: eager preparation left resolved shader " + std::to_string(index) + " unprepared"); std::ofstream file(output, std::ios::binary | std::ios::trunc); file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); Require(file.good(), "lazy resolved ABI: cannot write the eager reference"); + std::ofstream text(ReportPath(output), std::ios::trunc); + for (const auto& [key, value] : report) text << key << '=' << value << '\n'; + Require(text.good(), "lazy resolved ABI: cannot write the eager report"); } -void LazyResolution(const Layout& layout, const std::filesystem::path& reference) { +void LazyResolution(const Layout& layout, const std::filesystem::path& reference, const std::set& cached, const std::filesystem::path& cache) { std::vector expected(ColorBytes); { std::ifstream file(reference, std::ios::binary); file.read(reinterpret_cast(expected.data()), static_cast(expected.size())); Require(file.gcount() == static_cast(expected.size()), "lazy resolved ABI: the eager reference is missing or short"); } - ProgramArena arena(1u << 20u); - std::vector programs(Programs); - for (std::uint32_t index = 0; index < Programs; ++index) { - RegisterSynthetic(programs[index], arena, SyntheticPixelCode(index + 1, 0)); - AgcDriverResolveShaderAbi_nid_postfix(&programs[index].header.shader, {}, {}); - } - Settle(programs); - for (std::uint32_t index = 0; index < Programs; ++index) { - const auto entries = Entries(programs[index]); - Require(entries == 0, "lazy resolved ABI: ResolveShaderAbi prepared " + std::to_string(entries) + " entries for shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); + Report eager; + { + std::ifstream text(ReportPath(reference)); + for (std::string line; std::getline(text, line);) { + const auto split = line.find('='); + if (split != std::string::npos) eager[line.substr(0, split)] = line.substr(split + 1); + } + Require(eager.contains("libraries drawn"), "lazy resolved ABI: the eager report is missing or short"); } std::vector first; - for (std::uint32_t index = 0; index < Drawn; ++index) { - auto bytes = Draw(layout, programs[index]); - if (index == 0) first = std::move(bytes); - Require(Entries(programs[index]) != 0, "lazy resolved ABI: drawn shader " + std::to_string(index) + " holds no prepared entries"); + const auto lazy = Scenario(layout, first); + for (std::uint32_t index = 0; index <= Matching; ++index) { + const auto key = "resolved " + std::to_string(index); + Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: ResolveShaderAbi prepared " + std::to_string(Count(lazy.at(key))) + " entries for shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); + } + Require(lazy.at("libraries resolved") == lazy.at("libraries before"), "lazy resolved ABI: ResolveShaderAbi built pipeline libraries under APS5_LAZY_SHADER_PREPARE (" + lazy.at("libraries before") + " before, " + lazy.at("libraries resolved") + " after)"); + for (std::uint32_t index = 0; index <= Matching; ++index) Require(Count(lazy.at("registered " + std::to_string(index))) == 0, "lazy resolved ABI: registration prepared shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); + for (const auto index : DrawnPrograms) { + const auto expected = EagerResolutionEntries(eager, index); + const auto held = Prints(lazy.at("drawn " + std::to_string(index))); + Require(held == expected, "lazy resolved ABI: drawn shader " + std::to_string(index) + " holds other prepared entries than eager resolution and its draw made (eager " + Joined(expected) + "; lazy " + Joined(held) + ")"); } for (std::uint32_t index = Drawn; index < Programs; ++index) { - const auto entries = Entries(programs[index]); - Require(entries == 0, "lazy resolved ABI: shader " + std::to_string(index) + ", which never drew, holds " + std::to_string(entries) + " prepared entries"); + const auto key = "drawn " + std::to_string(index); + Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: shader " + std::to_string(index) + ", which never drew, holds " + std::to_string(Count(lazy.at(key))) + " prepared entries"); } - Require(std::ranges::any_of(first, [](std::uint8_t byte) { return byte != 0; }), "lazy resolved ABI: the drawn shader wrote nothing"); + Require(eager.at("drawn " + std::to_string(Matching)) == eager.at("resolved " + std::to_string(Matching)), "lazy resolved ABI: the draw whose state is its resolved ABI's did not find the ABI's entries with eager preparation"); + for (std::uint32_t index = 0; index < Drawn; ++index) Require(Count(eager.at("drawn " + std::to_string(index))) == Count(eager.at("resolved " + std::to_string(index))) + 1, "lazy resolved ABI: a draw whose state differs from its resolved ABI did not prepare its own entry with eager preparation"); + Require(lazy.at("libraries drawn") == eager.at("libraries drawn"), "lazy resolved ABI: the pipeline libraries built differ from eager preparation (eager " + eager.at("libraries drawn") + "; lazy " + lazy.at("libraries drawn") + ")"); Require(first == expected, "lazy resolved ABI: the shader prepared at its first draw drew other bytes than with eager preparation"); - std::printf("lazy resolved ABI: %u of %u ABI-resolved shaders drew; the %u that never drew hold no prepared entries, and a drawn one drew the same %zu bytes as with eager preparation\n", Drawn, Programs, Programs - Drawn, first.size()); + AgcDriverWaitIdle_nid_postfix(); + const auto files = CacheFiles(cache); + std::vector added; + std::ranges::set_difference(files, cached, std::back_inserter(added)); + Require(added.empty(), "lazy resolved ABI: preparation at first use wrote " + std::to_string(added.size()) + " shader cache files eager preparation had not, the first " + (added.empty() ? std::string() : added.front())); + std::printf("lazy resolved ABI: %u of %u ABI-resolved shaders drew; the %u that never drew hold no prepared entries; the drawn ones hold eager resolution's entries (the one drawn with its ABI's state found them, the others prepared their own), the pipeline libraries built match eager's (%s), a drawn one drew the same %zu bytes, and the lazy run wrote none of the %zu shader cache files eager wrote again\n", static_cast(DrawnPrograms.size()), Matching + 1, Programs - Drawn, lazy.at("libraries drawn").c_str(), first.size(), cached.size()); } } @@ -416,6 +529,17 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference int main(int argc, char** argv) { const bool eager = argc == 2 && std::string(argv[1]) == "--eager-reference"; std::filesystem::path reference; + std::filesystem::path cache; + std::set cached; + const auto clean = [&] { + if (eager) return; + std::error_code ignored; + if (!reference.empty()) { + std::filesystem::remove(reference, ignored); + std::filesystem::remove(ReportPath(reference), ignored); + } + if (!cache.empty()) std::filesystem::remove_all(cache, ignored); + }; try { { const auto device = OpenVulkanTestDevice(); @@ -426,11 +550,19 @@ int main(int argc, char** argv) { Require(path != nullptr, "lazy resolved ABI: the eager child has no reference path"); reference = path; } else { - reference = std::filesystem::temp_directory_path() / ("aps5-lazy-resolved-abi-" + std::to_string(std::random_device{}()) + ".bin"); + const auto run = std::to_string(std::random_device{}()); + reference = std::filesystem::temp_directory_path() / ("aps5-lazy-resolved-abi-" + run + ".bin"); + cache = std::filesystem::temp_directory_path() / ("aps5-lazy-resolved-abi-cache-" + run); + std::filesystem::create_directories(cache); SetEnvironment("LAZY_RESOLVED_ABI_REFERENCE", reference.string()); + SetEnvironment("ANYPS5_NO_SHADER_CACHE", "0"); + SetEnvironment("ANYPS5_SHADER_CACHE_DIR", cache.string()); + SetEnvironment("APS5_NO_PIPELINE_JOURNAL", "1"); ClearEnvironment("APS5_LAZY_SHADER_PREPARE"); const std::string command = "\"" + std::string(argv[0]) + "\" --eager-reference"; Require(std::system(command.c_str()) == 0, "lazy resolved ABI: the eager reference process failed"); + cached = CacheFiles(cache); + Require(!cached.empty(), "lazy resolved ABI: the eager reference process wrote no shader cache files"); SetEnvironment("APS5_LAZY_SHADER_PREPARE", "1"); } Register(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2); @@ -442,7 +574,7 @@ int main(int argc, char** argv) { Prepare(layout); Check(layout); if (eager) EagerReference(layout, reference); - else LazyResolution(layout, reference); + else LazyResolution(layout, reference, cached, cache); AgcDriverShutdown_nid_postfix(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; @@ -450,15 +582,9 @@ int main(int argc, char** argv) { AgcDriverShutdown_nid_postfix(); } catch (...) { } - if (!eager && !reference.empty()) { - std::error_code ignored; - std::filesystem::remove(reference, ignored); - } + clean(); return 1; } - if (!eager) { - std::error_code ignored; - std::filesystem::remove(reference, ignored); - } + clean(); return 0; } From 31cfb593e32fe0f5f30fcc5c988c76efdd9556b5 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:45:29 +0000 Subject: [PATCH 092/318] test(agc): compare stale-state cases of a deferred ABI with eager preparation agc_driver_lazy_resolved_abi's eager/lazy comparison now also covers the ways the state a deferred ABI keeps could differ at its first use: - the ABI's context and primitive register arrays are overwritten after resolution and before any draw; - one shader is resolved with two ABIs, the registered state and its draw's, before any use; - a second header is registered for a shader whose ABI is queued, a draw uses it, then the first header is registered again and drawn. Each drawn shader still ends with the entries eager resolution and its draws made: drawn with the ABI's state it finds them, the second header prepares its own, and the re-registered header finds its queued ABI's. --- .../tests/execution/LazyResolvedAbi.cpp | 72 +++++++++++++------ 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp index 636deb9230..4a2a829146 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp @@ -318,15 +318,15 @@ private: std::uint8_t* base = nullptr; }; -void RegisterSynthetic(Synthetic& program, ProgramArena& arena, const std::vector& code) { - program.code = arena.Place(code); +void RegisterHeader(Synthetic& program, std::uint32_t* code, std::size_t words) { + program.code = code; const auto address = reinterpret_cast(program.code); auto& header = program.header; header.registers = {{{0x008, static_cast(address >> 8u)}, {0x009, static_cast(address >> 40u)}, {0x00b, 8u << 1u}, {0x00a, 0}}}; header.shader.file_header = 0x34333231u; header.shader.version = 0x18u; header.shader.header_size = sizeof(SyntheticHeader); - header.shader.shader_size = static_cast(code.size() * sizeof(std::uint32_t)); + header.shader.shader_size = static_cast(words * sizeof(std::uint32_t)); header.shader.code = program.code; header.shader.type = 1; header.shader.user_data = &header.userData; @@ -337,6 +337,10 @@ void RegisterSynthetic(Synthetic& program, ProgramArena& arena, const std::vecto AgcDriverRegisterShader_nid_postfix(&header.shader); } +void RegisterSynthetic(Synthetic& program, ProgramArena& arena, const std::vector& code) { + RegisterHeader(program, arena.Place(code), code.size()); +} + constexpr std::uint32_t Programs = 4; constexpr std::uint32_t Drawn = 2; @@ -376,7 +380,14 @@ std::size_t Entries(const Synthetic& program) { } constexpr std::uint32_t Matching = Programs; -constexpr std::array DrawnPrograms{0, 1, Matching}; +constexpr std::uint32_t Twice = Programs + 1; +constexpr std::uint32_t Replaced = Programs + 2; +constexpr std::uint32_t Replacement = Programs + 3; +constexpr std::array DrawnPrograms{0, 1, Matching, Twice, Replaced, Replacement}; + +bool RegistrationAbi(std::uint32_t index) { + return index < Programs || index == Twice; +} using Report = std::map; @@ -420,7 +431,7 @@ std::vector EagerResolutionEntries(const Report& eager, std::uint32 const auto id = std::to_string(index); const auto registered = Prints(eager.at("registered " + id)); const auto drawn = Prints(eager.at("drawn " + id)); - if (Count(eager.at("resolved " + id)) == registered.size()) return drawn; + if (RegistrationAbi(index)) return drawn; std::vector made; std::ranges::set_difference(drawn, registered, std::back_inserter(made)); return made; @@ -431,24 +442,40 @@ Report Scenario(const Layout& layout, std::vector& first) { Report report; report["libraries before"] = Text(driver.PrebuiltLibraries()); ProgramArena arena(1u << 20u); - std::vector programs(Matching + 1); - for (std::uint32_t index = 0; index <= Matching; ++index) RegisterSynthetic(programs[index], arena, SyntheticPixelCode(index + 1, 0)); - Settle(programs); - for (std::uint32_t index = 0; index <= Matching; ++index) report["registered " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); - for (std::uint32_t index = 0; index < Programs; ++index) AgcDriverResolveShaderAbi_nid_postfix(&programs[index].header.shader, {}, {}); + std::vector programs(Replacement + 1); + const auto record = [&](const char* phase, std::uint32_t index) { + report[std::string(phase) + " " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + }; + const auto resolved = std::span(programs).first(Replaced + 1); + for (std::uint32_t index = 0; index <= Replaced; ++index) RegisterSynthetic(programs[index], arena, SyntheticPixelCode(index + 1, 0)); + Settle(resolved); + for (std::uint32_t index = 0; index <= Replaced; ++index) record("registered", index); std::vector context; for (const auto& [offset, value] : DrawContext(layout)) context.push_back({offset, value}); - const std::array primitive{{{PrimitiveType.first, PrimitiveType.second}}}; + std::array primitive{{{PrimitiveType.first, PrimitiveType.second}}}; + for (std::uint32_t index = 0; index < Programs; ++index) AgcDriverResolveShaderAbi_nid_postfix(&programs[index].header.shader, {}, {}); AgcDriverResolveShaderAbi_nid_postfix(&programs[Matching].header.shader, context, primitive); - Settle(programs); - for (std::uint32_t index = 0; index <= Matching; ++index) report["resolved " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + AgcDriverResolveShaderAbi_nid_postfix(&programs[Twice].header.shader, {}, {}); + AgcDriverResolveShaderAbi_nid_postfix(&programs[Twice].header.shader, context, primitive); + AgcDriverResolveShaderAbi_nid_postfix(&programs[Replaced].header.shader, context, primitive); + for (auto& reg : context) reg.value = ~reg.value; + primitive.front().value = ~primitive.front().value; + Settle(resolved); + for (std::uint32_t index = 0; index <= Replaced; ++index) record("resolved", index); report["libraries resolved"] = Text(driver.PrebuiltLibraries()); - for (const auto index : DrawnPrograms) { - auto bytes = Draw(layout, programs[index]); + RegisterHeader(programs[Replacement], programs[Replaced].code, programs[Replaced].header.shader.shader_size / sizeof(std::uint32_t)); + Settle(std::span(programs).subspan(Replacement, 1)); + record("registered", Replacement); + const auto draw = [&](std::uint32_t index, const Synthetic& program) { + auto bytes = Draw(layout, program); Require(std::ranges::any_of(bytes, [](std::uint8_t byte) { return byte != 0; }), "lazy resolved ABI: drawn shader " + std::to_string(index) + " wrote nothing"); if (index == 0) first = std::move(bytes); - } - for (std::uint32_t index = 0; index <= Matching; ++index) report["drawn " + std::to_string(index)] = Text(driver.PreparedEntryHashes(&programs[index].header.shader)); + }; + for (const auto index : {0u, 1u, Matching, Twice}) draw(index, programs[index]); + draw(Replacement, programs[Replacement]); + AgcDriverRegisterShader_nid_postfix(&programs[Replaced].header.shader); + draw(Replaced, programs[Replaced]); + for (std::uint32_t index = 0; index <= Replacement; ++index) record("drawn", index); report["libraries drawn"] = Text(driver.PrebuiltLibraries()); return report; } @@ -470,7 +497,7 @@ std::set CacheFiles(const std::filesystem::path& cache) { void EagerReference(const Layout& layout, const std::filesystem::path& output) { std::vector bytes; const auto report = Scenario(layout, bytes); - for (std::uint32_t index = 0; index <= Matching; ++index) Require(Count(report.at("resolved " + std::to_string(index))) != 0, "lazy resolved ABI: eager preparation left resolved shader " + std::to_string(index) + " unprepared"); + for (std::uint32_t index = 0; index <= Replaced; ++index) Require(Count(report.at("resolved " + std::to_string(index))) != 0, "lazy resolved ABI: eager preparation left resolved shader " + std::to_string(index) + " unprepared"); std::ofstream file(output, std::ios::binary | std::ios::trunc); file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); Require(file.good(), "lazy resolved ABI: cannot write the eager reference"); @@ -497,12 +524,12 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference } std::vector first; const auto lazy = Scenario(layout, first); - for (std::uint32_t index = 0; index <= Matching; ++index) { + for (std::uint32_t index = 0; index <= Replaced; ++index) { const auto key = "resolved " + std::to_string(index); Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: ResolveShaderAbi prepared " + std::to_string(Count(lazy.at(key))) + " entries for shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); } Require(lazy.at("libraries resolved") == lazy.at("libraries before"), "lazy resolved ABI: ResolveShaderAbi built pipeline libraries under APS5_LAZY_SHADER_PREPARE (" + lazy.at("libraries before") + " before, " + lazy.at("libraries resolved") + " after)"); - for (std::uint32_t index = 0; index <= Matching; ++index) Require(Count(lazy.at("registered " + std::to_string(index))) == 0, "lazy resolved ABI: registration prepared shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); + for (std::uint32_t index = 0; index <= Replacement; ++index) Require(Count(lazy.at("registered " + std::to_string(index))) == 0, "lazy resolved ABI: registration prepared shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); for (const auto index : DrawnPrograms) { const auto expected = EagerResolutionEntries(eager, index); const auto held = Prints(lazy.at("drawn " + std::to_string(index))); @@ -512,8 +539,9 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference const auto key = "drawn " + std::to_string(index); Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: shader " + std::to_string(index) + ", which never drew, holds " + std::to_string(Count(lazy.at(key))) + " prepared entries"); } - Require(eager.at("drawn " + std::to_string(Matching)) == eager.at("resolved " + std::to_string(Matching)), "lazy resolved ABI: the draw whose state is its resolved ABI's did not find the ABI's entries with eager preparation"); + for (const auto index : {Matching, Twice, Replaced}) Require(eager.at("drawn " + std::to_string(index)) == eager.at("resolved " + std::to_string(index)), "lazy resolved ABI: shader " + std::to_string(index) + ", drawn with its resolved ABI's state, did not find the ABI's entries with eager preparation"); for (std::uint32_t index = 0; index < Drawn; ++index) Require(Count(eager.at("drawn " + std::to_string(index))) == Count(eager.at("resolved " + std::to_string(index))) + 1, "lazy resolved ABI: a draw whose state differs from its resolved ABI did not prepare its own entry with eager preparation"); + Require(Count(eager.at("drawn " + std::to_string(Replacement))) == Count(eager.at("registered " + std::to_string(Replacement))) + 1, "lazy resolved ABI: the header registered over a resolved one did not prepare its own entry at its draw with eager preparation"); Require(lazy.at("libraries drawn") == eager.at("libraries drawn"), "lazy resolved ABI: the pipeline libraries built differ from eager preparation (eager " + eager.at("libraries drawn") + "; lazy " + lazy.at("libraries drawn") + ")"); Require(first == expected, "lazy resolved ABI: the shader prepared at its first draw drew other bytes than with eager preparation"); AgcDriverWaitIdle_nid_postfix(); @@ -521,7 +549,7 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference std::vector added; std::ranges::set_difference(files, cached, std::back_inserter(added)); Require(added.empty(), "lazy resolved ABI: preparation at first use wrote " + std::to_string(added.size()) + " shader cache files eager preparation had not, the first " + (added.empty() ? std::string() : added.front())); - std::printf("lazy resolved ABI: %u of %u ABI-resolved shaders drew; the %u that never drew hold no prepared entries; the drawn ones hold eager resolution's entries (the one drawn with its ABI's state found them, the others prepared their own), the pipeline libraries built match eager's (%s), a drawn one drew the same %zu bytes, and the lazy run wrote none of the %zu shader cache files eager wrote again\n", static_cast(DrawnPrograms.size()), Matching + 1, Programs - Drawn, lazy.at("libraries drawn").c_str(), first.size(), cached.size()); + std::printf("lazy resolved ABI: %u shaders drew, %u of them ABI-resolved; the %u resolved ones that never drew hold no prepared entries; the drawn ones hold the entries eager resolution and their draws made: drawn with their ABI's state they found them, with the ABI register arrays overwritten after resolution (one resolved with two ABIs, one whose header was registered over and again before its draw), drawn with another state they prepared their own; the pipeline libraries built match eager's (%s), a drawn one drew the same %zu bytes, and the lazy run wrote none of the %zu shader cache files eager wrote again\n", static_cast(DrawnPrograms.size()), static_cast(DrawnPrograms.size() - 1), Programs - Drawn, lazy.at("libraries drawn").c_str(), first.size(), cached.size()); } } From c01832242d8997ac451ce5dcdef3d404e60fe75a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:46:24 +0000 Subject: [PATCH 093/318] test(agc): check a deferred compute ABI under lazy preparation agc_driver_prepared_shaders_abi_deferred_compute resolves the ABI of a compute shader whose s_swappc_b64 calls a user-data pointer and requires the resolution to attempt its front end once. Under APS5_LAZY_SHADER_PREPARE=1 the resolution only plans the stage and leaves the attempt to the first dispatch, so with the switch exported the test failed on that expectation before reaching the dispatch. With the switch set the test now requires that neither resolution builds a front end, and still requires the dispatch to run both callees. agc_driver_prepared_shaders_abi_deferred_compute_lazy runs it so: the first dispatch logs the queued ABI's failure once, links the callees and writes 3 * (5 + 64) in every lane. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +++++ .../prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 10d7e03a0e..630a22b411 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2660,6 +2660,11 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_prepared_shaders_abi_deferred_compute_lazy COMMAND agc_driver_prepared_shaders_tests --abi-deferred-compute) + set_tests_properties(agc_driver_prepared_shaders_abi_deferred_compute_lazy PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_FAILURE_MEMO=1;APS5_LAZY_SHADER_PREPARE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_prepared_shaders_abi_deferred_compute_lazy PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered) set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 734a7c6825..6203b1706c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -958,11 +958,13 @@ void AbiDeferredCompute() { caller.shader.specials = &caller.specials; caller.specials.dispatch_modifier = 0x8000u; AgcDriverRegisterShader_nid_postfix(&caller.shader); + const bool lazy = std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr; const auto before = ShaderRecompiler::FrontEndBuilds(); AgcDriverResolveShaderAbi_nid_postfix(&caller.shader, {}, {}); const auto attempted = ShaderRecompiler::FrontEndBuilds(); AgcDriverResolveShaderAbi_nid_postfix(&caller.shader, {}, {}); - Require(attempted > before && ShaderRecompiler::FrontEndBuilds() == attempted, "a compute static ABI deferred to its dispatch was prepared again for the same state"); + if (lazy) Require(attempted == before && ShaderRecompiler::FrontEndBuilds() == attempted, "a compute static ABI resolved under lazy preparation built its front end before its dispatch"); + else Require(attempted > before && ShaderRecompiler::FrontEndBuilds() == attempted, "a compute static ABI deferred to its dispatch was prepared again for the same state"); const auto first = reinterpret_cast(AddFiveCallee.data()); const auto second = reinterpret_cast(AddSixtyFourCallee.data()); const auto output = reinterpret_cast(CallerOutput.data()); From dae468d260f1ed35d810ac3306a76639972ebca1 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sat, 17 Oct 2026 15:27:51 +0000 Subject: [PATCH 094/318] feat(agc): count whether a deferred ABI's first use finds its artifact Under APS5_LAZY_SHADER_PREPARE=1 a shader's first draw or dispatch prepares the ABIs ResolveShaderAbi queued on it, then looks for its own artifact. Whether that artifact is among the entries the queued ABIs just prepared decides whether the queued preparation is the use's own work or extra. Each first use that prepared queued ABIs now counts as found when the artifact it uses is among the entries that preparation added, or as own when it prepares its artifact itself. Entries prepared from queued ABIs are marked, and the first lookup that matches one counts it, so the entries no use ever matched are counted too. The counters are relaxed atomics touched only by a lookup that matched or prepared. On the first change and then at most once a minute stderr shows [shaders] deferred ABIs: N prepared at first use, F found their artifact there, O prepared their own; U of the E entries prepared from them never matched a use Driver::DeferredAbiUses returns the same counts. Test: agc_driver_lazy_resolved_abi checks that its two draws whose state differs from the resolved ABI count as own with every queued entry unmatched, and resolves a fifth shader's ABI with its draw's context and primitive registers, whose draw counts as found and matches one entry. --- .../Execution/include/Driver/Driver.hpp | 8 ++ .../include/Driver/Shaders/ShaderRegistry.hpp | 2 + .../src/Driver/Shaders/ShaderRegistry.cpp | 92 +++++++++++++++++-- .../tests/execution/LazyResolvedAbi.cpp | 24 ++++- 4 files changed, 114 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index ca5f882eb9..623130c420 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -68,6 +68,14 @@ public: void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType); void SettleShaderPreparations(std::span stages); [[nodiscard]] std::size_t PreparedEntries(const Shader* shader); + struct DeferredAbiUseCounts { + std::uint64_t firstUses; + std::uint64_t found; + std::uint64_t own; + std::uint64_t entries; + std::uint64_t unmatchedEntries; + }; + [[nodiscard]] static DeferredAbiUseCounts DeferredAbiUses(); [[nodiscard]] std::vector> PreparedEntryHashes(const Shader* shader); [[nodiscard]] std::array PrebuiltLibraries(); static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 07718b5ad6..6929f83cb2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -127,6 +127,8 @@ struct PreparedShaderState { struct Entry { std::size_t codeOffset; std::shared_ptr handle; + bool fromDeferredAbi = false; + bool matched = false; }; struct Rectangle { std::uint64_t vertexId; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 6cdad1b526..54286b1e7a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -597,7 +597,57 @@ bool LazyShaderPreparation() { return lazy; } -void PrepareDeferredAbis(const ShaderSnapshot& snapshot); +using PreparedAtUse = std::optional>; + +PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot); + +struct DeferredAbiUseCounters { + std::atomic firstUses{0}; + std::atomic found{0}; + std::atomic own{0}; + std::atomic entries{0}; + std::atomic matchedEntries{0}; + std::atomic lastReport{0}; +}; + +DeferredAbiUseCounters& DeferredAbiCounters() { + static DeferredAbiUseCounters counters; + return counters; +} + +void ReportDeferredAbiUses() { + auto& counters = DeferredAbiCounters(); + const auto now = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); + auto previous = counters.lastReport.load(std::memory_order_relaxed); + if ((previous != 0 && now - previous < 60000) || !counters.lastReport.compare_exchange_strong(previous, now, std::memory_order_relaxed)) return; + const auto entries = counters.entries.load(std::memory_order_relaxed); + const auto matched = counters.matchedEntries.load(std::memory_order_relaxed); + std::fprintf(stderr, "[shaders] deferred ABIs: %llu prepared at first use, %llu found their artifact there, %llu prepared their own; %llu of the %llu entries prepared from them never matched a use\n", static_cast(counters.firstUses.load(std::memory_order_relaxed)), static_cast(counters.found.load(std::memory_order_relaxed)), static_cast(counters.own.load(std::memory_order_relaxed)), static_cast(entries - std::min(entries, matched)), static_cast(entries)); +} + +void NoteMatchedEntry(PreparedShaders::Entry& entry, const PreparedAtUse& preparedAtUse) { + auto& counters = DeferredAbiCounters(); + bool changed = false; + if (entry.fromDeferredAbi && !entry.matched) { + entry.matched = true; + counters.matchedEntries.fetch_add(1, std::memory_order_relaxed); + changed = true; + } + if (preparedAtUse) { + counters.firstUses.fetch_add(1, std::memory_order_relaxed); + if (std::ranges::find(*preparedAtUse, entry.handle.get()) != preparedAtUse->end()) counters.found.fetch_add(1, std::memory_order_relaxed); + changed = true; + } + if (changed) ReportDeferredAbiUses(); +} + +void NoteOwnPreparation(const PreparedAtUse& preparedAtUse) { + if (!preparedAtUse) return; + auto& counters = DeferredAbiCounters(); + counters.firstUses.fetch_add(1, std::memory_order_relaxed); + counters.own.fetch_add(1, std::memory_order_relaxed); + ReportDeferredAbiUses(); +} void ReportPreparedAtUse(const ShaderSnapshot& snapshot, const ShaderRecompiler::RecompileRequest& request) { if (request.context.convertedSamples) { @@ -634,14 +684,19 @@ std::shared_ptr SourceHandleFor(const Shad ShaderRecompiler::BuildPreparedShaderKey(request, key); std::unique_lock lock(snapshot.prepared->mutex); AwaitRegisteredPreparation(*snapshot.prepared, lock); + PreparedAtUse preparedAtUse; if (!snapshot.prepared->deferredAbis.empty()) { lock.unlock(); - PrepareDeferredAbis(snapshot); + preparedAtUse = PrepareDeferredAbis(snapshot); lock.lock(); } - for (const auto& entry : snapshot.prepared->entries) { - if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) return entry.handle; + for (auto& entry : snapshot.prepared->entries) { + if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) { + NoteMatchedEntry(entry, preparedAtUse); + return entry.handle; + } } + NoteOwnPreparation(preparedAtUse); ReportPreparedAtUse(snapshot, request); auto handle = PrepareShaderWithDiagnostics(request); snapshot.prepared->entries.push_back({codeOffset, handle}); @@ -792,17 +847,22 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s struct PreparedKeyStorage {}; auto& key = HostThreadLocal, PreparedKeyStorage>(); ShaderRecompiler::BuildPreparedShaderKey(source, key); + PreparedAtUse preparedAtUse; if (!snapshot.prepared->deferredAbis.empty()) { lock.unlock(); - PrepareDeferredAbis(snapshot); + preparedAtUse = PrepareDeferredAbis(snapshot); lock.lock(); } - for (const auto& entry : snapshot.prepared->entries) { + for (auto& entry : snapshot.prepared->entries) { if (entry.codeOffset != codeOffset) continue; invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle); if (!linked.code.empty() && !std::ranges::equal(invocationRequest.shader.code, linked.code)) continue; - if (auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, entry.handle, key)) return std::move(*invocation); + if (auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, entry.handle, key)) { + NoteMatchedEntry(entry, preparedAtUse); + return std::move(*invocation); + } } + NoteOwnPreparation(preparedAtUse); ReportPreparedAtUse(snapshot, source); auto handle = PrepareShaderWithDiagnostics(source); invocationRequest = source; @@ -1089,12 +1149,14 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap return plan != nullptr ? PrepareRegistered(*plan) : std::vector{}; } -void PrepareDeferredAbis(const ShaderSnapshot& snapshot) { +PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot) { std::vector deferred; { std::lock_guard lock(snapshot.prepared->mutex); deferred.swap(snapshot.prepared->deferredAbis); } + if (deferred.empty()) return std::nullopt; + std::vector added; for (const auto& abi : deferred) { const auto device = abi.device.lock(); if (device == nullptr || snapshot.registeredState == nullptr) continue; @@ -1117,9 +1179,14 @@ void PrepareDeferredAbis(const ShaderSnapshot& snapshot) { std::lock_guard lock(snapshot.prepared->mutex); for (auto& entry : entries) { const auto duplicate = std::ranges::any_of(snapshot.prepared->entries, [&](const auto& existing) { return existing.codeOffset == entry.codeOffset && existing.handle->artifact == entry.handle->artifact; }); - if (!duplicate) snapshot.prepared->entries.push_back(std::move(entry)); + if (duplicate) continue; + entry.fromDeferredAbi = true; + added.push_back(entry.handle.get()); + snapshot.prepared->entries.push_back(std::move(entry)); } } + DeferredAbiCounters().entries.fetch_add(added.size(), std::memory_order_relaxed); + return added; } void DeferPipelineStage(PreparedShaderState& prepared, const ShaderSnapshot& snapshot, const char* reason) { @@ -1638,6 +1705,13 @@ std::size_t Driver::PreparedEntries(const Shader* shader) { return snapshot->prepared->entries.size(); } +Driver::DeferredAbiUseCounts Driver::DeferredAbiUses() { + auto& counters = DeferredAbiCounters(); + const auto entries = counters.entries.load(std::memory_order_relaxed); + const auto matched = counters.matchedEntries.load(std::memory_order_relaxed); + return {counters.firstUses.load(std::memory_order_relaxed), counters.found.load(std::memory_order_relaxed), counters.own.load(std::memory_order_relaxed), entries, entries - std::min(entries, matched)}; +} + std::vector> Driver::PreparedEntryHashes(const Shader* shader) { std::shared_ptr snapshot; { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp index 4a2a829146..aad7c87ddb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LazyResolvedAbi.cpp @@ -379,6 +379,10 @@ std::size_t Entries(const Synthetic& program) { return AgcDriver::DriverDetail::Driver::Get().PreparedEntries(&program.header.shader); } +std::string Uses(const AgcDriver::DriverDetail::Driver::DeferredAbiUseCounts& uses) { + return std::to_string(uses.firstUses) + " first uses, " + std::to_string(uses.found) + " found, " + std::to_string(uses.own) + " own, " + std::to_string(uses.unmatchedEntries) + " of " + std::to_string(uses.entries) + " entries unmatched"; +} + constexpr std::uint32_t Matching = Programs; constexpr std::uint32_t Twice = Programs + 1; constexpr std::uint32_t Replaced = Programs + 2; @@ -437,7 +441,7 @@ std::vector EagerResolutionEntries(const Report& eager, std::uint32 return made; } -Report Scenario(const Layout& layout, std::vector& first) { +Report Scenario(const Layout& layout, std::vector& first, std::array* uses = nullptr) { auto& driver = AgcDriver::DriverDetail::Driver::Get(); Report report; report["libraries before"] = Text(driver.PrebuiltLibraries()); @@ -471,7 +475,15 @@ Report Scenario(const Layout& layout, std::vector& first) { Require(std::ranges::any_of(bytes, [](std::uint8_t byte) { return byte != 0; }), "lazy resolved ABI: drawn shader " + std::to_string(index) + " wrote nothing"); if (index == 0) first = std::move(bytes); }; - for (const auto index : {0u, 1u, Matching, Twice}) draw(index, programs[index]); + const auto count = [&](std::size_t slot) { + if (uses != nullptr) (*uses)[slot] = AgcDriver::DriverDetail::Driver::DeferredAbiUses(); + }; + count(0); + for (const auto index : {0u, 1u}) draw(index, programs[index]); + count(1); + draw(Matching, programs[Matching]); + count(2); + draw(Twice, programs[Twice]); draw(Replacement, programs[Replacement]); AgcDriverRegisterShader_nid_postfix(&programs[Replaced].header.shader); draw(Replaced, programs[Replaced]); @@ -523,7 +535,8 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference Require(eager.contains("libraries drawn"), "lazy resolved ABI: the eager report is missing or short"); } std::vector first; - const auto lazy = Scenario(layout, first); + std::array uses{}; + const auto lazy = Scenario(layout, first, &uses); for (std::uint32_t index = 0; index <= Replaced; ++index) { const auto key = "resolved " + std::to_string(index); Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: ResolveShaderAbi prepared " + std::to_string(Count(lazy.at(key))) + " entries for shader " + std::to_string(index) + " under APS5_LAZY_SHADER_PREPARE"); @@ -539,6 +552,11 @@ void LazyResolution(const Layout& layout, const std::filesystem::path& reference const auto key = "drawn " + std::to_string(index); Require(Count(lazy.at(key)) == 0, "lazy resolved ABI: shader " + std::to_string(index) + ", which never drew, holds " + std::to_string(Count(lazy.at(key))) + " prepared entries"); } + const auto& [start, unmatched, matched] = uses; + Require(unmatched.firstUses - start.firstUses == Drawn && unmatched.own - start.own == Drawn && unmatched.found == start.found, "lazy resolved ABI: draws whose state differs from the resolved ABI did not count as preparing their own artifact (before: " + Uses(start) + "; after: " + Uses(unmatched) + ")"); + Require(unmatched.entries > start.entries && unmatched.unmatchedEntries - start.unmatchedEntries == unmatched.entries - start.entries, "lazy resolved ABI: entries prepared for an ABI no draw used do not count as unmatched (before: " + Uses(start) + "; after: " + Uses(unmatched) + ")"); + Require(matched.firstUses == unmatched.firstUses + 1 && matched.found == unmatched.found + 1 && matched.own == unmatched.own, "lazy resolved ABI: a draw whose state matches the resolved ABI did not count as finding its artifact (before: " + Uses(unmatched) + "; after: " + Uses(matched) + ")"); + Require(matched.entries > unmatched.entries && matched.unmatchedEntries == unmatched.unmatchedEntries + (matched.entries - unmatched.entries) - 1, "lazy resolved ABI: the entry a matching draw used still counts as unmatched (before: " + Uses(unmatched) + "; after: " + Uses(matched) + ")"); for (const auto index : {Matching, Twice, Replaced}) Require(eager.at("drawn " + std::to_string(index)) == eager.at("resolved " + std::to_string(index)), "lazy resolved ABI: shader " + std::to_string(index) + ", drawn with its resolved ABI's state, did not find the ABI's entries with eager preparation"); for (std::uint32_t index = 0; index < Drawn; ++index) Require(Count(eager.at("drawn " + std::to_string(index))) == Count(eager.at("resolved " + std::to_string(index))) + 1, "lazy resolved ABI: a draw whose state differs from its resolved ABI did not prepare its own entry with eager preparation"); Require(Count(eager.at("drawn " + std::to_string(Replacement))) == Count(eager.at("registered " + std::to_string(Replacement))) + 1, "lazy resolved ABI: the header registered over a resolved one did not prepare its own entry at its draw with eager preparation"); From 4eeeeb6438b466f3c229deb8d9425f3c7f2f9bd3 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:39:40 +0000 Subject: [PATCH 095/318] fix(agc): prepare a deferred ABI once when queues make its first use at once The first use of a shader whose ABI was deferred swapped the deferred list out and prepared it unlocked. A second queue reaching the shader meanwhile found the list empty and no entry yet, so it prepared an artifact of its own under the lock, and the first use then matched that artifact instead of the one its preparation made. Mark the shader while its deferred ABIs are prepared and have other uses wait on the settled condition until the entries are published. The entries of all deferred ABIs are published together under the lock. agc_driver_prepared_shaders_abi_deferred_concurrent_lazy submits the first dispatches of one deferred ABI from four queues at once and requires one first use that found its artifact, no own preparation, every front-end build and entry to come from the deferred ABI, and all four dispatches to use the same entry of it. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 ++ .../include/Driver/Shaders/ShaderRegistry.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 62 ++++++++------ .../tests/execution/PreparedShaders.cpp | 85 ++++++++++++++++++- 4 files changed, 124 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 630a22b411..567029b6c7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2660,6 +2660,11 @@ if(BUILD_TESTING) if(WIN32) set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_prepared_shaders_abi_deferred_concurrent_lazy COMMAND agc_driver_prepared_shaders_tests --abi-deferred-concurrent) + set_tests_properties(agc_driver_prepared_shaders_abi_deferred_concurrent_lazy PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_FAILURE_MEMO=1;APS5_LAZY_SHADER_PREPARE=1;APS5_SUSPEND_DRAIN=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_prepared_shaders_abi_deferred_concurrent_lazy PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_prepared_shaders_abi_deferred_compute_lazy COMMAND agc_driver_prepared_shaders_tests --abi-deferred-compute) set_tests_properties(agc_driver_prepared_shaders_abi_deferred_compute_lazy PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_FAILURE_MEMO=1;APS5_LAZY_SHADER_PREPARE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp index 6929f83cb2..8bd5c76db5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp @@ -162,6 +162,7 @@ struct PreparedShaders : PreparedShaderState { std::weak_ptr device; }; std::vector deferredAbis; + bool deferredPreparing = false; std::mutex mutex; std::condition_variable settled; std::exception_ptr failure; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 54286b1e7a..1dcc43ad67 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -599,7 +599,7 @@ bool LazyShaderPreparation() { using PreparedAtUse = std::optional>; -PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot); +PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot, std::unique_lock& lock); struct DeferredAbiUseCounters { std::atomic firstUses{0}; @@ -685,11 +685,7 @@ std::shared_ptr SourceHandleFor(const Shad std::unique_lock lock(snapshot.prepared->mutex); AwaitRegisteredPreparation(*snapshot.prepared, lock); PreparedAtUse preparedAtUse; - if (!snapshot.prepared->deferredAbis.empty()) { - lock.unlock(); - preparedAtUse = PrepareDeferredAbis(snapshot); - lock.lock(); - } + if (!snapshot.prepared->deferredAbis.empty() || snapshot.prepared->deferredPreparing) preparedAtUse = PrepareDeferredAbis(snapshot, lock); for (auto& entry : snapshot.prepared->entries) { if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) { NoteMatchedEntry(entry, preparedAtUse); @@ -848,11 +844,7 @@ ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& s auto& key = HostThreadLocal, PreparedKeyStorage>(); ShaderRecompiler::BuildPreparedShaderKey(source, key); PreparedAtUse preparedAtUse; - if (!snapshot.prepared->deferredAbis.empty()) { - lock.unlock(); - preparedAtUse = PrepareDeferredAbis(snapshot); - lock.lock(); - } + if (!snapshot.prepared->deferredAbis.empty() || snapshot.prepared->deferredPreparing) preparedAtUse = PrepareDeferredAbis(snapshot, lock); for (auto& entry : snapshot.prepared->entries) { if (entry.codeOffset != codeOffset) continue; invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle); @@ -1149,14 +1141,27 @@ std::vector PrepareRegistered(const ShaderSnapshot& snap return plan != nullptr ? PrepareRegistered(*plan) : std::vector{}; } -PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot) { - std::vector deferred; - { - std::lock_guard lock(snapshot.prepared->mutex); - deferred.swap(snapshot.prepared->deferredAbis); +PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot, std::unique_lock& lock) { + auto& prepared = *snapshot.prepared; + if (prepared.deferredPreparing) { + prepared.settled.wait(lock, [&] { return !prepared.deferredPreparing; }); + return std::nullopt; } - if (deferred.empty()) return std::nullopt; - std::vector added; + if (prepared.deferredAbis.empty()) return std::nullopt; + std::vector deferred; + deferred.swap(prepared.deferredAbis); + prepared.deferredPreparing = true; + struct Preparing { + PreparedShaders& prepared; + std::unique_lock& lock; + ~Preparing() { + if (!lock.owns_lock()) lock.lock(); + prepared.deferredPreparing = false; + prepared.settled.notify_all(); + } + } preparing{prepared, lock}; + lock.unlock(); + std::vector entries; for (const auto& abi : deferred) { const auto device = abi.device.lock(); if (device == nullptr || snapshot.registeredState == nullptr) continue; @@ -1166,24 +1171,25 @@ PreparedAtUse PrepareDeferredAbis(const ShaderSnapshot& snapshot) { state.userConfig = snapshot.registeredState->userConfig; for (const auto& [offset, value] : abi.context) state.context[offset] = value; for (const auto& [offset, value] : abi.primitive) state.userConfig[offset] = value; - std::vector entries; try { const auto plan = PlanRegistered(snapshot, *device, state, false); if (plan == nullptr) continue; plan->prebuildDevice = device; - entries = PrepareRegistered(*plan); + auto made = PrepareRegistered(*plan); + std::ranges::move(made, std::back_inserter(entries)); } catch (const std::exception& error) { APS5_LOG_ERR("Shader 0x%llx could not be prepared at its first use for its resolved ABI (%s); preparing it for the use instead", static_cast(snapshot.codeAddress), error.what()); continue; } - std::lock_guard lock(snapshot.prepared->mutex); - for (auto& entry : entries) { - const auto duplicate = std::ranges::any_of(snapshot.prepared->entries, [&](const auto& existing) { return existing.codeOffset == entry.codeOffset && existing.handle->artifact == entry.handle->artifact; }); - if (duplicate) continue; - entry.fromDeferredAbi = true; - added.push_back(entry.handle.get()); - snapshot.prepared->entries.push_back(std::move(entry)); - } + } + lock.lock(); + std::vector added; + for (auto& entry : entries) { + const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.codeOffset == entry.codeOffset && existing.handle->artifact == entry.handle->artifact; }); + if (duplicate) continue; + entry.fromDeferredAbi = true; + added.push_back(entry.handle.get()); + prepared.entries.push_back(std::move(entry)); } DeferredAbiCounters().entries.fetch_add(added.size(), std::memory_order_relaxed); return added; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 6203b1706c..c2aede2905 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -983,6 +983,88 @@ void AbiDeferredCompute() { AgcDriverShutdown_nid_postfix(); } +void AbiDeferredConcurrent() { + using namespace NarrowConstantStoreFixture; + Require(std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr, "lazy preparation is not enabled"); + struct Header { + Shader shader{}; + std::array registers{}; + ShaderSpecialRegs specials{}; + }; + static Header header; + const auto address = reinterpret_cast(Code.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18; + header.shader.header_size = sizeof(header); + header.shader.shader_size = sizeof(Code); + header.shader.code = Code.data(); + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + header.shader.specials = &header.specials; + header.specials.dispatch_modifier = 0x8000; + header.registers = {{{0x20c, static_cast(address >> 8u)}, {0x20d, static_cast(address >> 40u)}, {0x207, Threads}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 4}}}; + AgcDriverRegisterShader_nid_postfix(&header.shader); + AgcDriverResolveShaderAbi_nid_postfix(&header.shader, {}, {}); + constexpr std::size_t Queues = 4; + constexpr std::size_t OutputBytes = 65536; +#ifdef _WIN32 + auto* allocation = static_cast(VirtualAlloc(nullptr, Queues * OutputBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + auto* allocation = static_cast(std::aligned_alloc(OutputBytes, Queues * OutputBytes)); +#endif + Require(allocation != nullptr, "cannot allocate the concurrent first use outputs"); + const auto release = [](std::uint8_t* block) { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + VirtualFree(block, 0, MEM_RELEASE); +#else + std::free(block); +#endif + }; + const std::unique_ptr ownedOutputs(allocation, release); + GuestAllocations::Mutation().Add(allocation, Queues * OutputBytes, true, true, true); + std::fill_n(allocation, Queues * OutputBytes, Fill); + std::vector> commands(Queues); + std::vector packets; + for (std::size_t queue = 0; queue < Queues; ++queue) { + const auto output = reinterpret_cast(allocation + queue * OutputBytes); + for (const auto reg : header.registers) commands[queue].insert(commands[queue].end(), {0xc0017600u, reg.offset, reg.value}); + commands[queue].insert(commands[queue].end(), {0xc0027600u, 0x240, static_cast(output), static_cast(output >> 32u), 0xc0031500u, 1, 1, 1, 0x8041}); + packets.push_back({commands[queue].data(), static_cast(commands[queue].size()), 0, {}}); + } + const auto builds = ShaderRecompiler::FrontEndBuilds(); + const auto uses = AgcDriver::DriverDetail::Driver::DeferredAbiUses(); + std::barrier start(static_cast(Queues)); + std::vector threads; + for (std::size_t queue = 0; queue < Queues; ++queue) { + threads.emplace_back([&, queue] { + start.arrive_and_wait(); + sceAgcDriverSubmitAcb(0x20u + static_cast(queue), &packets[queue]); + }); + } + for (auto& thread : threads) thread.join(); + AgcDriverWaitIdle_nid_postfix(); + AgcDriverSuspendPoint_nid_postfix(); + AgcDriverWaitIdle_nid_postfix(); + for (std::size_t queue = 0; queue < Queues; ++queue) { + const auto* output = allocation + queue * OutputBytes; + for (std::size_t offset = 0; offset < OutputBytes; ++offset) { + const auto expected = offset < Threads * LaneBytes ? ExpectedLane[offset % LaneBytes] : Fill; + if (output[offset] != expected) throw std::runtime_error("concurrent first use: queue " + std::to_string(queue) + " byte " + std::to_string(offset) + " is " + std::to_string(output[offset]) + ", expected " + std::to_string(expected)); + } + } + const auto after = AgcDriver::DriverDetail::Driver::DeferredAbiUses(); + const auto built = ShaderRecompiler::FrontEndBuilds() - builds; + const auto entries = AgcDriver::DriverDetail::Driver::Get().PreparedEntries(&header.shader); + const auto matched = (after.entries - after.unmatchedEntries) - (uses.entries - uses.unmatchedEntries); + const auto counts = std::to_string(built) + " front-end builds, " + std::to_string(entries) + " prepared entries, " + std::to_string(after.firstUses - uses.firstUses) + " first uses, " + std::to_string(after.found - uses.found) + " found, " + std::to_string(after.own - uses.own) + " own, " + std::to_string(matched) + " entries used"; + std::cout << "concurrent first use of a deferred ABI: " << counts << '\n'; + Require(after.firstUses == uses.firstUses + 1 && after.found == uses.found + 1 && after.own == uses.own, ("four queues making the first use of one deferred ABI at once did not prepare it once (" + counts + ")").c_str()); + Require(built == after.entries - uses.entries && entries == built, ("four queues making the first use of one deferred ABI at once prepared an artifact besides its preparation (" + counts + ")").c_str()); + Require(matched == 1, ("four queues making the first use of one deferred ABI at once did not all use the same entry of its preparation (" + counts + ")").c_str()); + AgcDriverShutdown_nid_postfix(); +} + void AbiDeferredRegistrationFailure() { static AbiHeader store; DescribeAbi(store, WaterfallStoreCode, 0, {{0x20c, ProgramLow(WaterfallStoreCode.data())}, {0x20d, ProgramHigh(WaterfallStoreCode.data())}, {0x207, 64}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 2u << 1u}}, {}); @@ -1268,11 +1350,12 @@ void RelocatedHeaders() { int main(int argc, char** argv) { try { const std::string_view abiMode = argc == 2 ? argv[1] : ""; - const bool abiDeferred = abiMode == "--abi-deferred-compute" || abiMode == "--abi-deferred-pixel" || abiMode == "--abi-deferred-stages" || abiMode == "--abi-deferred-rectangle" || abiMode == "--abi-deferred-undecodable-state" || abiMode == "--abi-deferred-registration-failure" || abiMode == "--abi-deferred-vertex-inputs" || abiMode == "--abi-deferred-rectangle-pairs"; + const bool abiDeferred = abiMode == "--abi-deferred-compute" || abiMode == "--abi-deferred-concurrent" || abiMode == "--abi-deferred-pixel" || abiMode == "--abi-deferred-stages" || abiMode == "--abi-deferred-rectangle" || abiMode == "--abi-deferred-undecodable-state" || abiMode == "--abi-deferred-registration-failure" || abiMode == "--abi-deferred-vertex-inputs" || abiMode == "--abi-deferred-rectangle-pairs"; Require(argc == 1 || abiDeferred || (argc == 2 && (std::string_view(argv[1]) == "--indirect" || std::string_view(argv[1]) == "--fail-before-registration" || std::string_view(argv[1]) == "--deferred" || std::string_view(argv[1]) == "--deferred-undecodable" || std::string_view(argv[1]) == "--registered-workgroup" || std::string_view(argv[1]) == "--lazy")), "invalid test arguments"); if (abiDeferred) { if (!OpenVulkanTestDevice()) return VulkanTestSkipped; if (abiMode == "--abi-deferred-compute") AbiDeferredCompute(); + else if (abiMode == "--abi-deferred-concurrent") AbiDeferredConcurrent(); else if (abiMode == "--abi-deferred-pixel") AbiDeferredPixel(); else if (abiMode == "--abi-deferred-stages") AbiDeferredStages(); else if (abiMode == "--abi-deferred-undecodable-state") AbiDeferredUndecodableState(); From 1ba22061e3d648af18612080f70489c426073fb9 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 04:40:23 +0000 Subject: [PATCH 096/318] fix(agc): report deferred ABI uses only under APS5_PROFILE_DRAW The deferred ABI line went to stderr in every run that resolved an ABI at its first use. Print it through the profile output, like the [rescache] and [gpu] lines, and only when APS5_PROFILE_DRAW is set. The counters keep counting either way. --- .../Execution/src/Driver/Shaders/ShaderRegistry.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 1dcc43ad67..841b4b8610 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/LogMacros.hpp" #include "ControlFlow/GraphBuilder.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" @@ -616,13 +617,15 @@ DeferredAbiUseCounters& DeferredAbiCounters() { } void ReportDeferredAbiUses() { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (!profile) return; auto& counters = DeferredAbiCounters(); const auto now = std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count(); auto previous = counters.lastReport.load(std::memory_order_relaxed); if ((previous != 0 && now - previous < 60000) || !counters.lastReport.compare_exchange_strong(previous, now, std::memory_order_relaxed)) return; const auto entries = counters.entries.load(std::memory_order_relaxed); const auto matched = counters.matchedEntries.load(std::memory_order_relaxed); - std::fprintf(stderr, "[shaders] deferred ABIs: %llu prepared at first use, %llu found their artifact there, %llu prepared their own; %llu of the %llu entries prepared from them never matched a use\n", static_cast(counters.firstUses.load(std::memory_order_relaxed)), static_cast(counters.found.load(std::memory_order_relaxed)), static_cast(counters.own.load(std::memory_order_relaxed)), static_cast(entries - std::min(entries, matched)), static_cast(entries)); + AgcDriver::ProfilePrint_nid_no_patch("[shaders] deferred ABIs: %llu prepared at first use, %llu found their artifact there, %llu prepared their own; %llu of the %llu entries prepared from them never matched a use\n", static_cast(counters.firstUses.load(std::memory_order_relaxed)), static_cast(counters.found.load(std::memory_order_relaxed)), static_cast(counters.own.load(std::memory_order_relaxed)), static_cast(entries - std::min(entries, matched)), static_cast(entries)); } void NoteMatchedEntry(PreparedShaders::Entry& entry, const PreparedAtUse& preparedAtUse) { From 849c71646f9218cbe76b55895bcfcff5c2a172dd Mon Sep 17 00:00:00 2001 From: Mehran Date: Wed, 21 Oct 2026 07:58:49 +0300 Subject: [PATCH 097/318] fix(agc): keep the write-back ready barrier while hole copies are pending --- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index eb824c85d1..5f9c20af02 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -3415,7 +3415,7 @@ std::uint64_t StorageTexture::writeBackWindows(std::span se toRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; toRead.newLayout = VK_IMAGE_LAYOUT_GENERAL; const BarrierMask ready{VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, importReady.srcAccessMask | toRead.srcAccessMask, importReady.dstAccessMask | toRead.dstAccessMask, toRead.oldLayout != toRead.newLayout, toRead.srcQueueFamilyIndex != toRead.dstQueueFamilyIndex}; - const bool ordered = recorder != nullptr && seeds.empty() && padding.copies.empty() && Recorder::ElideCovered(Recorder::CommandClass::StorageWriteBack, ready, covered); + const bool ordered = recorder != nullptr && seeds.empty() && padding.copies.empty() && holeCopies.empty() && Recorder::ElideCovered(Recorder::CommandClass::StorageWriteBack, ready, covered); if (!ordered) context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &importReady, 0, nullptr, 1, &toRead); for (std::size_t i = 0; i < windows.size(); ++i) { const auto& window = windows[i]; From fa6f246a9b1695c03dd8ada3e987096b635a0a90 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 01:59:31 +0700 Subject: [PATCH 098/318] fix(shader): flush the operands of image, buffer and global float atomics On RDNA2 the image, buffer and global float atomics (fmin, fmax, fcmpswap and the _x2 forms) flush both operands to a signed zero when bit 0 of the shader's denormal field is clear, as the LDS forms do: the f32 field for the 32-bit forms, the f16/f64 field for _x2. Only the LDS forms flushed here. floatAtomicFlush (was sharedFloatFlush) now sets MemoryInfo::flushDenormals for every float atomic, and AtomicFloatBits applies it in minMax and compareSwap. The float atomic tests from #332, #506 and #562 were measured with denormals kept, so they now set FLOAT_MODE 0xf0. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/BufferAtomicsFloat.cpp | 1 + .../tests/execution/GlobalAtomicsFloat.cpp | 1 + .../tests/execution/ImageAtomicsFloat.cpp | 1 + .../execution/MemoryFloatAtomicModes.cpp | 452 ++++++++++++++++++ .../SpirvBackend/SpirvEmitterHelpers.hpp | 4 +- .../SpirvBackend/src/SpirvImageEmitter.cpp | 4 +- .../SpirvMemory/SpirvMemoryInstructions.cpp | 66 +-- .../Translation/TranslationContext.hpp | 2 +- .../MemoryInstructions/BufferInstructions.cpp | 3 +- .../MemoryInstructions/FlatInstructions.cpp | 3 +- .../MemoryInstructions/ImageInstructions.cpp | 3 +- .../MemoryResourceHelpers.cpp | 31 ++ .../SharedMemoryInstructions.cpp | 22 +- docs/dev/debt/shader.md | 5 +- 14 files changed, 538 insertions(+), 60 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicsFloat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicsFloat.cpp index 99a286d6ac..01b015a01b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicsFloat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicsFloat.cpp @@ -149,6 +149,7 @@ void Run(AgcDriver::VulkanDevice& device) { {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp index 503ccf662f..b4827a3b53 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalAtomicsFloat.cpp @@ -684,6 +684,7 @@ void Dispatch(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const std {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}; const auto result = ShaderRecompiler::Recompile(request); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomicsFloat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomicsFloat.cpp index a7b492f8e8..78b36356c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomicsFloat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomicsFloat.cpp @@ -504,6 +504,7 @@ ShaderRecompiler::RecompileResult Compile(std::uint32_t waveSize, const ShaderRe {0, 0, 0, 128} }; request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}; return ShaderRecompiler::Recompile(request); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp new file mode 100644 index 0000000000..aa4f0afb67 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp @@ -0,0 +1,452 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "prx/libc/include/GuestAllocations.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Inputs = 12; +constexpr std::uint32_t Results = 32; +constexpr std::uint32_t TargetDwords = 512; +constexpr std::uint32_t Format32Float = 22; +constexpr std::uint32_t Type2D = 9; +constexpr std::size_t BlockBytes = 4096; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array BufferCode{ + 0x160200b0u, 0x34180087u, 0xe0381000u, 0x80000201u, 0xe0381010u, 0x80000601u, 0xe0341020u, 0x80000a01u, + 0xbf8c3f70u, 0x341a0082u, 0x4a1a1affu, 0x00001000u, 0x341c0083u, 0x4a1c1cffu, 0x00001200u, 0xe0701000u, + 0x8001020du, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xe0fc5000u, 0x8001100du, 0xbf8c3f70u, 0xe0305000u, + 0x8001120du, 0xbf8c3f70u, 0xe0701000u, 0x8001100cu, 0xe0701004u, 0x8001120cu, 0xe0701080u, 0x8001020du, + 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xe1005080u, 0x8001100du, 0xbf8c3f70u, 0xe0305080u, 0x8001120du, + 0xbf8c3f70u, 0xe0701008u, 0x8001100cu, 0xe070100cu, 0x8001120cu, 0xe0701100u, 0x8001020du, 0xbbfd0000u, + 0x7e200303u, 0x7e220304u, 0xe0f85100u, 0x8001100du, 0xbf8c3f70u, 0xe0305100u, 0x8001120du, 0xbf8c3f70u, + 0xe0701010u, 0x8001100cu, 0xe0701014u, 0x8001120cu, 0xe0741000u, 0x8001060eu, 0xbbfd0000u, 0x7e280308u, + 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, 0xe17c5000u, 0x8001140eu, 0xbf8c3f70u, 0xe0345000u, 0x8001180eu, + 0xbf8c3f70u, 0xe0741018u, 0x8001140cu, 0xe0741020u, 0x8001180cu, 0xe0741100u, 0x8001060eu, 0xbbfd0000u, + 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, 0xe1805100u, 0x8001140eu, 0xbf8c3f70u, 0xe0345100u, + 0x8001180eu, 0xbf8c3f70u, 0xe0741028u, 0x8001140cu, 0xe0741030u, 0x8001180cu, 0xe0741200u, 0x8001060eu, + 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, 0xe1785200u, 0x8001140eu, 0xbf8c3f70u, + 0xe0345200u, 0x8001180eu, 0xbf8c3f70u, 0xe0741038u, 0x8001140cu, 0xe0741040u, 0x8001180cu, 0xbf810000u, +}; + +alignas(256) constexpr std::array ImageCode{ + 0x160200b0u, 0x34180087u, 0xe0381000u, 0x80000201u, 0xe0381010u, 0x80000601u, 0xe0341020u, 0x80000a01u, + 0xbf8c3f70u, 0x7e3c0300u, 0x7e3e0280u, 0x7e260302u, 0xf03c0108u, 0x0002131eu, 0x7e200303u, 0x7e220304u, + 0xf0782108u, 0x0002101eu, 0x7e240280u, 0xf0442108u, 0x0002121eu, 0xbf8c3f70u, 0xe0701000u, 0x8001100cu, + 0xe0701004u, 0x8001120cu, 0x7e3e0281u, 0x7e260302u, 0xf03c0108u, 0x0002131eu, 0x7e200303u, 0x7e220304u, + 0xf07c2108u, 0x0002101eu, 0x7e240280u, 0xf0442108u, 0x0002121eu, 0xbf8c3f70u, 0xe0701008u, 0x8001100cu, + 0xe070100cu, 0x8001120cu, 0x7e3e0282u, 0x7e260302u, 0xf03c0108u, 0x0002131eu, 0x7e200303u, 0x7e220304u, + 0xf0742308u, 0x0002101eu, 0x7e240280u, 0xf0442108u, 0x0002121eu, 0xbf8c3f70u, 0xe0701010u, 0x8001100cu, + 0xe0701014u, 0x8001120cu, 0xbf810000u, +}; + +alignas(256) constexpr std::array GlobalCode{ + 0x160200b0u, 0x34180087u, 0xe0381000u, 0x80000201u, 0xe0381010u, 0x80000601u, 0xe0341020u, 0x80000a01u, + 0xbf8c3f70u, 0x34340082u, 0xd70f6a1au, 0x00023408u, 0x7e360209u, 0x50363680u, 0x34380083u, 0xd70f6a1cu, + 0x00023808u, 0x7e3a0209u, 0x503a3a80u, 0xdc708000u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, + 0xdcfd8000u, 0x137d101au, 0xbf8c3f70u, 0xdc318000u, 0x127d001au, 0xbf8c3f70u, 0xe0701000u, 0x8001130cu, + 0xe0701004u, 0x8001120cu, 0xdc708080u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xdd018080u, + 0x137d101au, 0xbf8c3f70u, 0xdc318080u, 0x127d001au, 0xbf8c3f70u, 0xe0701008u, 0x8001130cu, 0xe070100cu, + 0x8001120cu, 0xdc708100u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xdcf98100u, 0x137d101au, + 0xbf8c3f70u, 0xdc318100u, 0x127d001au, 0xbf8c3f70u, 0xe0701010u, 0x8001130cu, 0xe0701014u, 0x8001120cu, + 0xdc748200u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, 0xdd7d8200u, + 0x207d141cu, 0xbf8c3f70u, 0xdc358200u, 0x187d001cu, 0xbf8c3f70u, 0xe0741018u, 0x8001200cu, 0xe0741020u, + 0x8001180cu, 0xdc748300u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, + 0xdd818300u, 0x207d141cu, 0xbf8c3f70u, 0xdc358300u, 0x187d001cu, 0xbf8c3f70u, 0xe0741028u, 0x8001200cu, + 0xe0741030u, 0x8001180cu, 0xdc748400u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, + 0x7e2e030bu, 0xdd798400u, 0x207d141cu, 0xbf8c3f70u, 0xdc358400u, 0x187d001cu, 0xbf8c3f70u, 0xe0741038u, + 0x8001200cu, 0xe0741040u, 0x8001180cu, 0xbf810000u, +}; + +constexpr std::uint32_t Rows32[Threads][3] = { + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x80000000u, 0x80000000u}, + {0x80000000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000001u, 0x00000000u}, + {0x00000000u, 0x80000001u, 0x80000001u}, + {0x00000001u, 0x00000000u, 0x00000001u}, + {0x00000001u, 0x3f800000u, 0x3f800000u}, + {0x80000001u, 0x80000000u, 0x80000000u}, + {0x00000001u, 0x80000001u, 0x80000001u}, + {0x007fffffu, 0x00800000u, 0x007fffffu}, + {0x807fffffu, 0xbf800000u, 0x807fffffu}, + {0x7fc00000u, 0x00000001u, 0x7fc00000u}, + {0x00000001u, 0x7fc00000u, 0x00000001u}, + {0x7f800001u, 0x00000001u, 0x7f800001u}, + {0x00000001u, 0x7f800001u, 0x00000001u}, + {0x3f800000u, 0xbf800000u, 0x3f800000u}, + {0x7f800000u, 0xff800000u, 0xff800000u}, + {0x80000000u, 0x00000001u, 0x80000000u}, + {0x7fc12345u, 0x7fc00000u, 0x7fc12345u}, + {0xffc00000u, 0x3f800000u, 0x3f800000u}, + {0x7fa00000u, 0x3f800000u, 0x3f800000u}, + {0x40490fdbu, 0x007fffffu, 0x40490fdbu}, + {0x807fffffu, 0x80000001u, 0x80000001u}, + {0x00800000u, 0x007fffffu, 0x00800000u}, + {0x00000000u, 0xff800001u, 0xff800001u}, + {0x80000001u, 0x00000001u, 0x80000001u}, + {0x7f7fffffu, 0xff7fffffu, 0xff7fffffu}, + {0x00000001u, 0x00000001u, 0x00000001u}, + {0x007fffffu, 0x80000000u, 0x80000000u}, + {0xff800000u, 0x80000001u, 0x80000001u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x3f800000u, 0x00000001u, 0x3f800000u} +}; + +constexpr std::uint64_t Rows64[Threads][3] = { + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000000ull, 0x8000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x8000000000000001ull, 0x8000000000000001ull}, + {0x0000000000000001ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x0000000000000001ull, 0x3ff0000000000000ull, 0x3ff0000000000000ull}, + {0x8000000000000001ull, 0x8000000000000000ull, 0x8000000000000000ull}, + {0x0000000000000001ull, 0x8000000000000001ull, 0x8000000000000001ull}, + {0x000fffffffffffffull, 0x0010000000000000ull, 0x000fffffffffffffull}, + {0x800fffffffffffffull, 0xbff0000000000000ull, 0x800fffffffffffffull}, + {0x7ff8000000000000ull, 0x0000000000000001ull, 0x7ff8000000000000ull}, + {0x0000000000000001ull, 0x7ff8000000000000ull, 0x7ff8000000000000ull}, + {0x7ff0000000000001ull, 0x0000000000000001ull, 0x7ff0000000000001ull}, + {0x0000000000000001ull, 0x7ff0000000000001ull, 0x7ff0000000000001ull}, + {0x3ff0000000000000ull, 0xbff0000000000000ull, 0x3ff0000000000000ull}, + {0x7ff0000000000000ull, 0xfff0000000000000ull, 0x7ff0000000000000ull}, + {0x8000000000000000ull, 0x0000000000000001ull, 0x8000000000000000ull}, + {0x7ff8000000012345ull, 0x7ff8000000000000ull, 0x7ff8000000000000ull}, + {0xfff8000000000000ull, 0x3ff0000000000000ull, 0x3ff0000000000000ull}, + {0xfff0000000000001ull, 0x3ff0000000000000ull, 0x3ff0000000000000ull}, + {0x400921fb54442d18ull, 0x000fffffffffffffull, 0x400921fb54442d18ull}, + {0x800fffffffffffffull, 0x8000000000000001ull, 0x8000000000000001ull}, + {0x0010000000000000ull, 0x000fffffffffffffull, 0x0010000000000000ull}, + {0x0000000000000000ull, 0xfff0000000000001ull, 0x0000000000000000ull}, + {0x8000000000000001ull, 0x0000000000000001ull, 0x8000000000000001ull}, + {0x7fefffffffffffffull, 0xfff0000000000000ull, 0x7fefffffffffffffull}, + {0x0000000000000001ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x000fffffffffffffull, 0x8000000000000000ull, 0x8000000000000000ull}, + {0xfff0000000000000ull, 0x8000000000000001ull, 0x8000000000000001ull}, + {0x7ff8000000000000ull, 0x7ff8000000000000ull, 0x7ff8000000000000ull}, + {0x3ff0000000000000ull, 0x0000000000000001ull, 0x3ff0000000000000ull} +}; + +constexpr std::uint32_t Flushed32[Threads][3] = { + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x00000000u, 0x80000000u}, + {0x80000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x00000001u}, + {0x80000000u, 0x00000000u, 0x80000001u}, + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x3f800000u, 0x00000001u}, + {0x80000000u, 0x80000000u, 0x80000000u}, + {0x80000000u, 0x00000000u, 0x80000001u}, + {0x00000000u, 0x00800000u, 0x00800000u}, + {0xbf800000u, 0x80000000u, 0xbf800000u}, + {0x00000000u, 0x00000000u, 0x7fc00000u}, + {0x00000000u, 0x00000000u, 0x7fc00000u}, + {0x7fc00001u, 0x7fc00001u, 0x7f800001u}, + {0x7fc00001u, 0x7fc00001u, 0x7f800001u}, + {0xbf800000u, 0x3f800000u, 0xbf800000u}, + {0xff800000u, 0x7f800000u, 0x7f800000u}, + {0x80000000u, 0x00000000u, 0x00000001u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc12345u}, + {0x3f800000u, 0x3f800000u, 0xffc00000u}, + {0x7fe00000u, 0x7fe00000u, 0x7fa00000u}, + {0x00000000u, 0x40490fdbu, 0x007fffffu}, + {0x80000000u, 0x80000000u, 0x80000001u}, + {0x00000000u, 0x00800000u, 0x007fffffu}, + {0xffc00001u, 0xffc00001u, 0x00000000u}, + {0x80000000u, 0x00000000u, 0x00000001u}, + {0xff7fffffu, 0x7f7fffffu, 0x7f7fffffu}, + {0x00000000u, 0x00000000u, 0x00000001u}, + {0x80000000u, 0x00000000u, 0x80000000u}, + {0xff800000u, 0x80000000u, 0xff800000u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x00000000u, 0x3f800000u, 0x00000001u} +}; + +constexpr std::uint32_t Kept32[Threads][3] = { + {0x00000000u, 0x00000000u, 0x00000000u}, + {0x80000000u, 0x00000000u, 0x80000000u}, + {0x80000000u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000001u, 0x00000001u}, + {0x80000001u, 0x00000000u, 0x00000000u}, + {0x00000000u, 0x00000001u, 0x00000000u}, + {0x00000001u, 0x3f800000u, 0x00000001u}, + {0x80000001u, 0x80000000u, 0x80000001u}, + {0x80000001u, 0x00000001u, 0x00000001u}, + {0x007fffffu, 0x00800000u, 0x00800000u}, + {0xbf800000u, 0x807fffffu, 0xbf800000u}, + {0x00000001u, 0x00000001u, 0x7fc00000u}, + {0x00000001u, 0x00000001u, 0x7fc00000u}, + {0x7fc00001u, 0x7fc00001u, 0x7f800001u}, + {0x7fc00001u, 0x7fc00001u, 0x7f800001u}, + {0xbf800000u, 0x3f800000u, 0xbf800000u}, + {0xff800000u, 0x7f800000u, 0x7f800000u}, + {0x80000000u, 0x00000001u, 0x00000001u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc12345u}, + {0x3f800000u, 0x3f800000u, 0xffc00000u}, + {0x7fe00000u, 0x7fe00000u, 0x7fa00000u}, + {0x007fffffu, 0x40490fdbu, 0x007fffffu}, + {0x807fffffu, 0x80000001u, 0x807fffffu}, + {0x007fffffu, 0x00800000u, 0x007fffffu}, + {0xffc00001u, 0xffc00001u, 0x00000000u}, + {0x80000001u, 0x00000001u, 0x00000001u}, + {0xff7fffffu, 0x7f7fffffu, 0x7f7fffffu}, + {0x00000001u, 0x00000001u, 0x00000001u}, + {0x80000000u, 0x007fffffu, 0x007fffffu}, + {0xff800000u, 0x80000001u, 0xff800000u}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x00000001u, 0x3f800000u, 0x00000001u} +}; + +constexpr std::uint64_t Flushed64[Threads][3] = { + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000001ull}, + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x3ff0000000000000ull, 0x0000000000000001ull}, + {0x8000000000000000ull, 0x8000000000000000ull, 0x8000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000001ull}, + {0x0000000000000000ull, 0x0010000000000000ull, 0x0010000000000000ull}, + {0xbff0000000000000ull, 0x8000000000000000ull, 0xbff0000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull, 0x7ff8000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0x7ff0000000000001ull}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0x0000000000000001ull}, + {0xbff0000000000000ull, 0x3ff0000000000000ull, 0xbff0000000000000ull}, + {0xfff0000000000000ull, 0x7ff0000000000000ull, 0xfff0000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x7ff8000000000000ull, 0x7ff8000000000000ull, 0x7ff8000000012345ull}, + {0x3ff0000000000000ull, 0x3ff0000000000000ull, 0xfff8000000000000ull}, + {0xfff8000000000001ull, 0xfff8000000000001ull, 0xfff0000000000001ull}, + {0x0000000000000000ull, 0x400921fb54442d18ull, 0x000fffffffffffffull}, + {0x8000000000000000ull, 0x8000000000000000ull, 0x8000000000000001ull}, + {0x0000000000000000ull, 0x0010000000000000ull, 0x000fffffffffffffull}, + {0xfff8000000000001ull, 0xfff8000000000001ull, 0xfff0000000000001ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0xfff0000000000000ull, 0x7fefffffffffffffull, 0xfff0000000000000ull}, + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000001ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000000ull}, + {0xfff0000000000000ull, 0x8000000000000000ull, 0xfff0000000000000ull}, + {0x7ff8000000000000ull, 0x7ff8000000000000ull, 0x7ff8000000000000ull}, + {0x0000000000000000ull, 0x3ff0000000000000ull, 0x0000000000000001ull} +}; + +constexpr std::uint64_t Kept64[Threads][3] = { + {0x0000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x8000000000000000ull}, + {0x8000000000000000ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x8000000000000001ull, 0x0000000000000000ull, 0x0000000000000000ull}, + {0x0000000000000000ull, 0x0000000000000001ull, 0x0000000000000000ull}, + {0x0000000000000001ull, 0x3ff0000000000000ull, 0x0000000000000001ull}, + {0x8000000000000001ull, 0x8000000000000000ull, 0x8000000000000001ull}, + {0x8000000000000001ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x000fffffffffffffull, 0x0010000000000000ull, 0x0010000000000000ull}, + {0xbff0000000000000ull, 0x800fffffffffffffull, 0xbff0000000000000ull}, + {0x0000000000000001ull, 0x0000000000000001ull, 0x7ff8000000000000ull}, + {0x0000000000000001ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0x7ff0000000000001ull}, + {0x7ff8000000000001ull, 0x7ff8000000000001ull, 0x0000000000000001ull}, + {0xbff0000000000000ull, 0x3ff0000000000000ull, 0xbff0000000000000ull}, + {0xfff0000000000000ull, 0x7ff0000000000000ull, 0xfff0000000000000ull}, + {0x8000000000000000ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x7ff8000000000000ull, 0x7ff8000000000000ull, 0x7ff8000000012345ull}, + {0x3ff0000000000000ull, 0x3ff0000000000000ull, 0xfff8000000000000ull}, + {0xfff8000000000001ull, 0xfff8000000000001ull, 0xfff0000000000001ull}, + {0x000fffffffffffffull, 0x400921fb54442d18ull, 0x000fffffffffffffull}, + {0x800fffffffffffffull, 0x8000000000000001ull, 0x800fffffffffffffull}, + {0x000fffffffffffffull, 0x0010000000000000ull, 0x000fffffffffffffull}, + {0xfff8000000000001ull, 0xfff8000000000001ull, 0xfff0000000000001ull}, + {0x8000000000000001ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0xfff0000000000000ull, 0x7fefffffffffffffull, 0xfff0000000000000ull}, + {0x0000000000000001ull, 0x0000000000000001ull, 0x0000000000000001ull}, + {0x8000000000000000ull, 0x000fffffffffffffull, 0x000fffffffffffffull}, + {0xfff0000000000000ull, 0x8000000000000001ull, 0xfff0000000000000ull}, + {0x7ff8000000000000ull, 0x7ff8000000000000ull, 0x7ff8000000000000ull}, + {0x0000000000000001ull, 0x3ff0000000000000ull, 0x0000000000000001ull} +}; + +constexpr std::array Names{"fmin", "fmax", "fcmpswap"}; + +enum class Kind { Buffer, Image, Global }; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(std::aligned_alloc(BlockBytes, BlockBytes)); +#endif + Require(block != nullptr, "memory float atomic modes: cannot allocate the guest block"); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + } + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); + } + GuestBlock(const GuestBlock&) = delete; + GuestBlock& operator=(const GuestBlock&) = delete; + std::uint8_t* Data() { return block; } +private: + std::uint8_t* block = nullptr; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::array TextureDescriptor(const void* data) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Threads - 1u) & 3u) << 30u), + ((Threads - 1u) >> 2u) | (2u << 14u), + 0xfacu | (Type2D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::string Hex(std::uint64_t value, int digits) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%0*llx", digits, static_cast(value)); + return text; +} + +const char* KindName(Kind kind) { + return kind == Kind::Buffer ? "buffer" : kind == Kind::Image ? "image" : "global"; +} + +void FillInput() { + Input.fill(0u); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* row = &Input[tid * Inputs]; + row[0] = Rows32[tid][0]; + row[1] = Rows32[tid][1]; + row[2] = Rows32[tid][2]; + for (std::uint32_t i = 0; i < 3u; ++i) { + row[4u + 2u * i] = static_cast(Rows64[tid][i]); + row[5u + 2u * i] = static_cast(Rows64[tid][i] >> 32u); + } + } +} + +void Run(AgcDriver::VulkanDevice& device, Kind kind, GuestBlock& guest, const std::optional& floatMode) { + Output.fill(0xdeadbeefu); + Texels.fill(0xdeadbeefu); + std::memset(guest.Data(), 0xcd, BlockBytes); + std::vector userData(16, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + if (kind == Kind::Image) { + const auto texture = TextureDescriptor(Texels.data()); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + } else if (kind == Kind::Global) { + const auto address = static_cast(reinterpret_cast(guest.Data())); + userData[8] = static_cast(address); + userData[9] = static_cast(address >> 32u); + } + const std::span code = kind == Kind::Buffer ? std::span(BufferCode) : kind == Kind::Image ? std::span(ImageCode) : std::span(GlobalCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(Kind kind, const char* mode, bool flush32, bool flush64) { + const auto where = std::string("memory float atomic modes: ") + KindName(kind) + ", " + mode + ", lane "; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t op = 0; op < 3u; ++op) { + const std::uint32_t expected = flush32 ? Flushed32[tid][op] : Kept32[tid][op]; + Require(out[2u * op] == Rows32[tid][0], where + std::to_string(tid) + " " + Names[op] + " returned " + Hex(out[2u * op], 8) + ", expected " + Hex(Rows32[tid][0], 8)); + Require(out[2u * op + 1u] == expected, where + std::to_string(tid) + " " + Names[op] + " stored " + Hex(out[2u * op + 1u], 8) + ", expected " + Hex(expected, 8)); + } + if (kind == Kind::Image) continue; + for (std::uint32_t op = 0; op < 3u; ++op) { + const std::uint64_t returned = out[6u + 4u * op] | (static_cast(out[7u + 4u * op]) << 32u); + const std::uint64_t stored = out[8u + 4u * op] | (static_cast(out[9u + 4u * op]) << 32u); + const std::uint64_t expected = flush64 ? Flushed64[tid][op] : Kept64[tid][op]; + Require(returned == Rows64[tid][0], where + std::to_string(tid) + " " + Names[op] + "_x2 returned " + Hex(returned, 16) + ", expected " + Hex(Rows64[tid][0], 16)); + Require(stored == expected, where + std::to_string(tid) + " " + Names[op] + "_x2 stored " + Hex(stored, 16) + ", expected " + Hex(expected, 16)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityInt64Atomics)) { + std::puts("skipped, the device has no shaderBufferInt64Atomics"); + return VulkanTestSkipped; + } + GuestBlock guest; + FillInput(); + struct Mode { + std::optional floatMode; + const char* name; + bool flush32; + bool flush64; + }; + const std::array modes{{ + {std::nullopt, "no float mode", true, false}, + {ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, "FLOAT_MODE 0xc0", true, false}, + {ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, "FLOAT_MODE 0xf0", false, false}, + {ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}, "FLOAT_MODE 0x00", true, true}, + {ShaderRecompiler::ShaderFloatMode{0x50u, true, false, false}, "FLOAT_MODE 0x50", false, false}, + {ShaderRecompiler::ShaderFloatMode{0xa0u, true, false, false}, "FLOAT_MODE 0xa0", true, true}, + }}; + for (const auto kind : {Kind::Buffer, Kind::Image, Kind::Global}) { + for (const auto& mode : modes) { + Run(*device, kind, guest, mode.floatMode); + Check(kind, mode.name, mode.flush32, mode.flush64); + } + } + std::puts("memory float atomic modes tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 96951cf815..34213a41b6 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -161,8 +161,8 @@ std::uint32_t Select(SpirvEmitterState& state, std::uint32_t type, std::uint32_t std::uint32_t AtomicIncrement(SpirvEmitterState& state, std::uint32_t old, std::uint32_t limit); std::uint32_t AtomicDecrement(SpirvEmitterState& state, std::uint32_t old, std::uint32_t limit); std::uint32_t AtomicUSubSat(SpirvEmitterState& state, std::uint32_t old, std::uint32_t value); -std::uint32_t AtomicFloatMinMax(SpirvEmitterState& state, std::uint32_t old, std::uint32_t source, bool maxValue); -std::uint32_t AtomicFloatCompareSwap(SpirvEmitterState& state, std::uint32_t old, std::uint32_t desired, std::uint32_t comparator); +std::uint32_t AtomicFloatMinMax(SpirvEmitterState& state, std::uint32_t old, std::uint32_t source, bool maxValue, bool flush); +std::uint32_t AtomicFloatCompareSwap(SpirvEmitterState& state, std::uint32_t old, std::uint32_t desired, std::uint32_t comparator, bool flush); template std::uint32_t EmitNative(SpirvEmitterState& state, TArguments... arguments) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 772f4f8f4c..feaaa4ce80 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -1122,13 +1122,13 @@ void EmitAtomicOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { } if (opcode == IrOpcode::ImageAtomicFMin32 || opcode == IrOpcode::ImageAtomicFMax32) { return AtomicUpdate(state, pointer, ResourceKind::Image, [&](std::uint32_t current) { - return AtomicFloatMinMax(state, current, value, opcode == IrOpcode::ImageAtomicFMax32); + return AtomicFloatMinMax(state, current, value, opcode == IrOpcode::ImageAtomicFMax32, access.mem.flushDenormals); }); } if (opcode == IrOpcode::ImageAtomicFCmpSwap32) { const auto comparator = ctx.Arg(access.inst, 3); return AtomicUpdate(state, pointer, ResourceKind::Image, [&](std::uint32_t current) { - return AtomicFloatCompareSwap(state, current, value, comparator); + return AtomicFloatCompareSwap(state, current, value, comparator, access.mem.flushDenormals); }); } const auto old = state.module.AllocateId(); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index d997a174eb..cd82fa2998 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -957,6 +957,7 @@ std::uint32_t BufferAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst) { struct AtomicFloatBits { SpirvEmitterState& state; bool wide; + bool flushInputs = false; std::uint32_t type() const { return wide ? TypeScalarU64(state) : TypeU32(state); } std::uint32_t constant(std::uint64_t value) const { return wide ? state.module.Constant(spv::OpConstant, TypeScalarU64(state), static_cast(value), static_cast(value >> 32u)) : ConstantU32(state, static_cast(value)); @@ -986,6 +987,8 @@ struct AtomicFloatBits { return Binary(state, spv::OpLogicalOr, TypeBool(state), same, zeros); } std::uint32_t minMax(std::uint32_t old, std::uint32_t source, bool maxValue) const { + old = flush(old, flushInputs); + source = flush(source, flushInputs); const auto better = test(maxValue ? spv::OpUGreaterThan : spv::OpULessThan, orderKey(source), orderKey(old)); auto result = select(better, source, old); result = select(nan(old), source, result); @@ -995,7 +998,7 @@ struct AtomicFloatBits { return select(signalingNan(source), op(spv::OpBitwiseOr, source, constant(quiet())), result); } std::uint32_t compareSwap(std::uint32_t old, std::uint32_t comparator, std::uint32_t desired) const { - return select(equal(old, comparator), desired, old); + return select(equal(flush(old, flushInputs), flush(comparator, flushInputs)), desired, old); } std::uint32_t increment(std::uint32_t old, std::uint32_t limit) const { return select(test(spv::OpUGreaterThanEqual, old, limit), constant(0u), op(spv::OpIAdd, old, constant(1u))); @@ -1007,8 +1010,9 @@ struct AtomicFloatBits { }; std::uint32_t BufferFloatAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, bool maxValue) { - return EmitAtomicUpdate(ctx, inst, BufferMemory(ctx, inst), [maxValue](SpirvEmitterState& state, std::uint32_t old, std::uint32_t value) { - return AtomicFloatBits{state, false}.minMax(old, value, maxValue); + const auto& mem = BufferMemory(ctx, inst); + return EmitAtomicUpdate(ctx, inst, mem, [maxValue, flush = mem.flushDenormals](SpirvEmitterState& state, std::uint32_t old, std::uint32_t value) { + return AtomicFloatBits{state, false, flush}.minMax(old, value, maxValue); }); } @@ -1044,8 +1048,7 @@ std::uint32_t SharedFloatMinMax(SpirvValueEmitContext& ctx, const IrValue& inst, const auto data = ctx.Arg(inst, 1); const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomicUpdate(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { - const AtomicFloatBits bits{state, false}; - return bits.minMax(bits.flush(old, flush), bits.flush(data, flush), maxValue); + return AtomicFloatBits{state, false, flush}.minMax(old, data, maxValue); }); } @@ -1089,7 +1092,7 @@ std::uint32_t AddressAtomicOpcode(IrOpcode opcode) { } } -std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { +std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator, bool flush) { switch (opcode) { case IrOpcode::AddressAtomicSwap32: case IrOpcode::AddressAtomicCmpSwap32: @@ -1110,11 +1113,11 @@ std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::u case IrOpcode::AddressAtomicUSubSat32: return AtomicUSubSat(state, old, value); case IrOpcode::AddressAtomicFMin32: - return AtomicFloatBits{state, false}.minMax(old, value, false); + return AtomicFloatBits{state, false, flush}.minMax(old, value, false); case IrOpcode::AddressAtomicFMax32: - return AtomicFloatBits{state, false}.minMax(old, value, true); + return AtomicFloatBits{state, false, flush}.minMax(old, value, true); case IrOpcode::AddressAtomicFCmpSwap32: - return AtomicFloatBits{state, false}.compareSwap(old, comparator, value); + return AtomicFloatBits{state, false, flush}.compareSwap(old, comparator, value); default: throw std::runtime_error("FlatAtomicNext32: opcode has no aperture lowering"); } @@ -1122,6 +1125,7 @@ std::uint32_t FlatAtomicNext32(SpirvEmitterState& state, IrOpcode opcode, std::u std::uint32_t ApertureAtomic32(SpirvValueEmitContext& ctx, const IrValue& inst, ResourceKind kind, std::uint32_t address, std::uint32_t byteOffset, std::uint32_t value, std::uint32_t comparator) { auto& state = ctx.state; + const bool flush = ctx.Memory(inst).flushDenormals; const auto instruction = ConstantU32(state, inst.Flags().pc); const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteOffset, ConstantU32(state, 3u)), ConstantU32(state, 0u)); EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 4u), instruction, BdaAbi::FaultReason::Unaligned); }); @@ -1136,20 +1140,20 @@ std::uint32_t ApertureAtomic32(SpirvValueEmitContext& ctx, const IrValue& inst, return EmitValueOrZeroIfCondition(state, EmitMemoryElementInBounds(state, resource, index), [&] { const auto pointer = EmitMemoryElementPointer(state, resource, index); if (kind == ResourceKind::Lds) { - const auto update = [&]() { return AtomicUpdate(state, pointer, kind, [&](std::uint32_t current) { return FlatAtomicNext32(state, opcode, current, value, comparator); }); }; + const auto update = [&]() { return AtomicUpdate(state, pointer, kind, [&](std::uint32_t current) { return FlatAtomicNext32(state, opcode, current, value, comparator, flush); }); }; return state.requirements.ldsLock ? LockedLdsUpdate(state, TypeU32(state), ConstantU32(state, 0u), update) : update(); } const auto old = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), old, pointer); - state.module.AddFunction(spv::OpStore, pointer, FlatAtomicNext32(state, opcode, old, value, comparator)); + state.module.AddFunction(spv::OpStore, pointer, FlatAtomicNext32(state, opcode, old, value, comparator, flush)); return old; }); }); } -std::uint32_t FlatAtomicNext64(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator) { +std::uint32_t FlatAtomicNext64(SpirvEmitterState& state, IrOpcode opcode, std::uint32_t old, std::uint32_t value, std::uint32_t comparator, bool flush) { const auto u64 = TypeScalarU64(state); - const AtomicFloatBits bits{state, true}; + const AtomicFloatBits bits{state, true, flush}; const auto winner = [&](spv::Op compare) { return Select(state, u64, Binary(state, compare, TypeBool(state), old, value), old, value); }; @@ -1193,6 +1197,7 @@ std::uint32_t FlatAtomicNext64(SpirvEmitterState& state, IrOpcode opcode, std::u std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, ResourceKind kind, std::uint32_t address, std::uint32_t byteOffset, std::uint32_t value, std::uint32_t comparator) { auto& state = ctx.state; + const bool flush = ctx.Memory(inst).flushDenormals; const auto instruction = ConstantU32(state, inst.Flags().pc); const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteOffset, ConstantU32(state, 7u)), ConstantU32(state, 0u)); EmitIfCondition(state, unaligned, [&] { RecordBdaFault(state, address, ConstantU32(state, 8u), instruction, BdaAbi::FaultReason::Unaligned); }); @@ -1207,7 +1212,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, const auto opcode = inst.Opcode(); return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&] { return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&] { - if (kind == ResourceKind::Lds && state.requirements.ldsBufferInt64Atomics) return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return FlatAtomicNext64(state, opcode, old, value, comparator); }); + if (kind == ResourceKind::Lds && state.requirements.ldsBufferInt64Atomics) return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return FlatAtomicNext64(state, opcode, old, value, comparator, flush); }); const auto update = [&]() { const auto lowPointer = EmitMemoryElementPointer(state, resource, low); const auto highPointer = EmitMemoryElementPointer(state, resource, high); @@ -1217,7 +1222,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, state.module.AddFunction(spv::OpLoad, TypeU32(state), highValue, highPointer); const auto old = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, TypeU64(state), old, lowValue, highValue); - const auto next = Unary(state, spv::OpBitcast, TypeU64(state), FlatAtomicNext64(state, opcode, Unary(state, spv::OpBitcast, TypeScalarU64(state), old), value, comparator)); + const auto next = Unary(state, spv::OpBitcast, TypeU64(state), FlatAtomicNext64(state, opcode, Unary(state, spv::OpBitcast, TypeScalarU64(state), old), value, comparator, flush)); const auto nextLow = state.module.AllocateId(); const auto nextHigh = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), nextLow, next, 0u); @@ -1254,7 +1259,7 @@ std::uint32_t AddressAtomic(SpirvValueEmitContext& ctx, const IrValue& inst) { if (inst.Opcode() == IrOpcode::AddressAtomicUSubSat32) { return AtomicUpdate(state, pointer, mem.kind, [&](std::uint32_t current) { return AtomicUSubSat(state, current, value); }); } - const AtomicFloatBits bits{state, wide}; + const AtomicFloatBits bits{state, wide, mem.flushDenormals}; const auto update = [&](auto&& replacement) { return AtomicUpdateTyped(state, pointer, mem.kind, scalarType, replacement); }; switch (inst.Opcode()) { case IrOpcode::AddressAtomicFCmpSwap32: @@ -1491,8 +1496,7 @@ std::uint32_t SharedAtomic64Select(SpirvValueEmitContext& ctx, const IrValue& in std::uint32_t SharedFloatMinMax64(SpirvValueEmitContext& ctx, const IrValue& inst, bool maxValue) { const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomic64Binary(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old, std::uint32_t data) { - const AtomicFloatBits bits{state, true}; - return bits.minMax(bits.flush(old, flush), bits.flush(data, flush), maxValue); + return AtomicFloatBits{state, true, flush}.minMax(old, data, maxValue); }); } @@ -1522,13 +1526,12 @@ std::uint32_t AtomicUSubSat(SpirvEmitterState& state, std::uint32_t old, std::ui return Select(state, TypeU32(state), fits, difference, ConstantU32(state, 0u)); } -std::uint32_t AtomicFloatMinMax(SpirvEmitterState& state, std::uint32_t old, std::uint32_t source, bool maxValue) { - return AtomicFloatBits{state, false}.minMax(old, source, maxValue); +std::uint32_t AtomicFloatMinMax(SpirvEmitterState& state, std::uint32_t old, std::uint32_t source, bool maxValue, bool flush) { + return AtomicFloatBits{state, false, flush}.minMax(old, source, maxValue); } -std::uint32_t AtomicFloatCompareSwap(SpirvEmitterState& state, std::uint32_t old, std::uint32_t desired, std::uint32_t comparator) { - const AtomicFloatBits bits{state, false}; - return bits.select(bits.equal(old, comparator), desired, old); +std::uint32_t AtomicFloatCompareSwap(SpirvEmitterState& state, std::uint32_t old, std::uint32_t desired, std::uint32_t comparator, bool flush) { + return AtomicFloatBits{state, false, flush}.compareSwap(old, comparator, desired); } std::uint32_t EmitReadConst(SpirvValueEmitContext& ctx, const IrValue& inst) { @@ -1948,8 +1951,7 @@ std::uint32_t EmitSharedAtomicCmpstF32(SpirvValueEmitContext& ctx, const IrValue const auto source = ctx.Arg(inst, 2); const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomicUpdate(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { - const AtomicFloatBits bits{state, false}; - return bits.select(bits.equal(bits.flush(old, flush), bits.flush(compare, flush)), source, old); + return AtomicFloatBits{state, false, flush}.compareSwap(old, compare, source); }); } @@ -2051,8 +2053,7 @@ std::uint32_t EmitSharedAtomicCmpstF64(SpirvValueEmitContext& ctx, const IrValue const auto source = ScalarU64Argument(ctx, inst, 2u); const bool flush = SharedMemory(ctx, inst).flushDenormals; return SharedAtomic64(ctx, inst, [&](SpirvEmitterState& state, std::uint32_t old) { - const AtomicFloatBits bits{state, true}; - return bits.select(bits.equal(bits.flush(old, flush), bits.flush(compare, flush)), source, old); + return AtomicFloatBits{state, true, flush}.compareSwap(old, compare, source); }); } @@ -2118,24 +2119,27 @@ std::uint32_t EmitBufferAtomicFCmpSwap32(SpirvValueEmitContext& ctx, const IrVal const auto comparator = ctx.Arg(inst, inst.ArgumentCount() - 2u); const auto& mem = BufferMemory(ctx, inst); return EmitAtomicAccess(ctx, inst, mem, [&](std::uint32_t pointer) { - return AtomicUpdate(ctx.state, pointer, mem.kind, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, false}.compareSwap(old, comparator, desired); }); + return AtomicUpdate(ctx.state, pointer, mem.kind, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, false, mem.flushDenormals}.compareSwap(old, comparator, desired); }); }); } std::uint32_t EmitBufferAtomicFCmpSwap64(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto desired = ScalarU64Argument(ctx, inst, 3u); const auto comparator = ScalarU64Argument(ctx, inst, 2u); - return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true}.compareSwap(old, comparator, desired); }); + const bool flush = BufferMemory(ctx, inst).flushDenormals; + return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true, flush}.compareSwap(old, comparator, desired); }); } std::uint32_t EmitBufferAtomicFMin64(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto value = ScalarU64Argument(ctx, inst, 2u); - return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true}.minMax(old, value, false); }); + const bool flush = BufferMemory(ctx, inst).flushDenormals; + return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true, flush}.minMax(old, value, false); }); } std::uint32_t EmitBufferAtomicFMax64(SpirvValueEmitContext& ctx, const IrValue& inst) { const auto value = ScalarU64Argument(ctx, inst, 2u); - return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true}.minMax(old, value, true); }); + const bool flush = BufferMemory(ctx, inst).flushDenormals; + return BufferAtomic64Update(ctx, inst, [&](std::uint32_t old) { return AtomicFloatBits{ctx.state, true, flush}.minMax(old, value, true); }); } std::uint32_t EmitBufferAtomicInc64(SpirvValueEmitContext& ctx, const IrValue& inst) { diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index a7748044c4..d856fad819 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -112,6 +112,7 @@ private: std::array writeMask(const RdnaOperand& operand, IrU1 value, bool write64 = false); void writeCompareResult(const RdnaOperand& operand, IrU1 value); MemoryFlags addMemoryInfo(const MemoryInfo& memory, std::uint32_t pc); + bool floatAtomicFlush(IrOpcode opcode) const; ExportFlags addExportInfo(const RdnaInstruction& inst); AddressOperands readAddressOperands(const RdnaInstruction& inst, std::uint32_t firstSource); IrU32 getResourceDword(std::uint32_t index, std::uint32_t dword); @@ -157,7 +158,6 @@ private: bool dsWrite2(const RdnaInstruction& inst); bool dsSrc2(const RdnaInstruction& inst, IrOpcode opcode); bool dsAtomic2(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue); - bool sharedFloatFlush(IrOpcode opcode) const; bool dsAtomic64(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue); bool dsCondxchg32(const RdnaInstruction& inst); bool dsAppendConsume(const RdnaInstruction& inst, IrOpcode opcode); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp index 0b6d08a2c3..680ad0ab17 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/BufferInstructions.cpp @@ -203,7 +203,8 @@ bool TranslationContext::bufferStore(const RdnaInstruction& inst) { } bool TranslationContext::bufferAtomic(const RdnaInstruction& inst, IrOpcode opcode) { - const MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + MemoryInfo memory = bufferMemoryInfoFromInstruction(inst); + memory.flushDenormals = floatAtomicFlush(opcode); IrValue* resource = getBufferResource(memory); const BufferAddress address = readBufferAddress(inst); const MemoryFlags flags = addMemoryInfo(memory, inst.programCounter); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp index 862c65f29b..1115a0615a 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp @@ -179,7 +179,8 @@ bool TranslationContext::flatStore(const RdnaInstruction& inst) { } bool TranslationContext::flatAtomic(const RdnaInstruction& inst, IrOpcode opcode) { - const MemoryInfo memory = flatMemoryInfoFromInstruction(inst); + MemoryInfo memory = flatMemoryInfoFromInstruction(inst); + memory.flushDenormals = floatAtomicFlush(opcode); const AddressOperands address = readAddressOperands(inst, 0u); const MemoryFlags flags = addMemoryInfo(memory, inst.programCounter); IrValue& active = ir.GetExec(); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index c20f9b798c..7815ee9ccd 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp @@ -92,7 +92,8 @@ bool TranslationContext::imageAtomic(const RdnaInstruction& inst, IrOpcode opcod if (inst.dataBits == 64u) { opcode = imageAtomic64Opcode(opcode); } - const MemoryInfo memory = imageMemoryInfoFromInstruction(inst); + MemoryInfo memory = imageMemoryInfoFromInstruction(inst); + memory.flushDenormals = floatAtomicFlush(opcode); IrValue* resource = getImageResource(memory); IrValue* address = makeImageAddress(inst, inst.source0); const MemoryFlags flags = addMemoryInfo(memory, inst.programCounter); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp index bff55b085e..c04d0770fe 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryResourceHelpers.cpp @@ -32,6 +32,37 @@ MemoryFlags TranslationContext::addMemoryInfo(const MemoryInfo& memory, std::uin return MemoryFlags{index, pc}; } +bool TranslationContext::floatAtomicFlush(IrOpcode opcode) const { + switch (opcode) { + case IrOpcode::SharedAtomicFMin32: + case IrOpcode::SharedAtomicFMax32: + case IrOpcode::SharedAtomicCmpstF32: + case IrOpcode::SharedAtomicFAdd32: + case IrOpcode::BufferAtomicFMin32: + case IrOpcode::BufferAtomicFMax32: + case IrOpcode::BufferAtomicFCmpSwap32: + case IrOpcode::AddressAtomicFMin32: + case IrOpcode::AddressAtomicFMax32: + case IrOpcode::AddressAtomicFCmpSwap32: + case IrOpcode::ImageAtomicFMin32: + case IrOpcode::ImageAtomicFMax32: + case IrOpcode::ImageAtomicFCmpSwap32: + return !floatMode.has_value() || ((floatMode->floatMode >> 4u) & 1u) == 0u; + case IrOpcode::SharedAtomicFMin64: + case IrOpcode::SharedAtomicFMax64: + case IrOpcode::SharedAtomicCmpstF64: + case IrOpcode::BufferAtomicFMin64: + case IrOpcode::BufferAtomicFMax64: + case IrOpcode::BufferAtomicFCmpSwap64: + case IrOpcode::AddressAtomicFMin64: + case IrOpcode::AddressAtomicFMax64: + case IrOpcode::AddressAtomicFCmpSwap64: + return floatMode.has_value() && ((floatMode->floatMode >> 6u) & 1u) == 0u; + default: + return false; + } +} + TranslationContext::AddressOperands TranslationContext::readAddressOperands(const RdnaInstruction& inst, std::uint32_t firstSource) { const ResourceKind kind = flatSegmentResourceKind(inst); const IrU32 low = readU32(sourceAt(inst, firstSource)); diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp index a781713ddc..a79d94da4c 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp @@ -32,7 +32,7 @@ MemoryInfo sharedMemoryInfoFromInstruction(const RdnaInstruction& inst) { bool TranslationContext::dsAtomic(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue) { MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); - memory.flushDenormals = sharedFloatFlush(opcode); + memory.flushDenormals = floatAtomicFlush(opcode); memory.flushDenormalResults = opcode == IrOpcode::SharedAtomicFAdd32 && (!floatMode.has_value() || ((floatMode->floatMode >> 5u) & 1u) == 0u); const IrU32 address = readU32(inst.source0); const IrU32 value = readU32(inst.source1); @@ -213,25 +213,9 @@ bool TranslationContext::dsSrc2(const RdnaInstruction& inst, IrOpcode opcode) { return true; } -bool TranslationContext::sharedFloatFlush(IrOpcode opcode) const { - switch (opcode) { - case IrOpcode::SharedAtomicFMin32: - case IrOpcode::SharedAtomicFMax32: - case IrOpcode::SharedAtomicCmpstF32: - case IrOpcode::SharedAtomicFAdd32: - return !floatMode.has_value() || ((floatMode->floatMode >> 4u) & 1u) == 0u; - case IrOpcode::SharedAtomicFMin64: - case IrOpcode::SharedAtomicFMax64: - case IrOpcode::SharedAtomicCmpstF64: - return floatMode.has_value() && ((floatMode->floatMode >> 6u) & 1u) == 0u; - default: - return false; - } -} - bool TranslationContext::dsAtomic2(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue) { MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); - memory.flushDenormals = sharedFloatFlush(opcode); + memory.flushDenormals = floatAtomicFlush(opcode); const IrU32 address = readU32(inst.source0); const IrU32 data0 = readU32(inst.source1); const IrU32 data1 = readU32(inst.source2); @@ -246,7 +230,7 @@ bool TranslationContext::dsAtomic64(const RdnaInstruction& inst, IrOpcode opcode throw std::runtime_error("64-bit GDS atomics are not supported"); } MemoryInfo memory = sharedMemoryInfoFromInstruction(inst); - memory.flushDenormals = sharedFloatFlush(opcode); + memory.flushDenormals = floatAtomicFlush(opcode); const IrU32 address = readU32(inst.source0); const IrU64 data0 = readU64(inst.source1); IrValue& active = ir.GetExec(); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c842891c66..0d8ac810ee 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -110,7 +110,7 @@ - [f16 unary, frexp and conversion denormals](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follow the `FLOAT_MODE` f16 denormal field: `v_ceil_f16`, `v_floor_f16`, `v_fract_f16`, `v_rcp_f16`, `v_rsq_f16`, `v_sqrt_f16`, `v_log_f16`, `v_exp_f16`, `v_sin_f16`, `v_frexp_mant_f16`, `v_frexp_exp_i16_f16`, `v_cvt_norm_i16_f16`, `v_cvt_norm_u16_f16` and `v_cvt_f32_f16` flush a denormal half input to its signed zero when bit 0 is clear (0 and 2), and the f16-result ones flush a denormal half result to its signed zero when bit 1 is clear (0 and 1), after the host rounds it to half. Measured on RDNA2 (gfx1036) at IEEE=0 and IEEE=1 with denorm16 0..3 over every non-NaN f16 input (63,490 rows per mode and instruction): the only mismatches are the 1-ULP driver differences of `v_exp_f16` (2 inputs) and `v_sin_f16` (14), which are the same with denormals kept. NaN inputs are not compared. `v_ldexp_f16` shares the input rule. `v_cvt_pknorm_i16_f16` and `v_cvt_pknorm_u16_f16` flush both half inputs by the same bit, measured on RDNA2 (gfx1030) over every f16 input (each half also negated in the other source) at IEEE=0 and IEEE=1 with denorm16 0..3 and at f16 rounding 1..3: every row matches. Shaders without a known float mode keep the previous lowering. - [v_div_fixup_f16](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) flushes its src1 and src2 to a zero of the same sign when bit 0 of the `FLOAT_MODE` f16 denormal field is clear (0 and 2) and keeps a denormal src0 and result in every mode, as `v_div_fixup_f32` does with the f32 field. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) over 6859 triples of 19 halves (zeros, denormals, minimum normals, ±0.5, ±1, the largest finite value, infinities and quiet and signaling NaNs) at IEEE=0 and IEEE=1 with denorm16 0..3 and FP16_OVFL 0 and 1, also with neg and abs source modifiers and with op_sel high halves. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding throws. In the f16 denormal modes that flush, each denormal input lane is flushed to a zero of its sign when bit 0 of the field is clear (0 and 2), so `inf * 0x0001` is an invalid product there, and when bit 1 is clear (0 and 1) a `v_pk_add_f16` result below 2^-14, or a `v_pk_mul_f16`, `v_pk_fma_f16` or `v_pk_fmac_f16` result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, is flushed to a zero of its sign; `v_pk_min/max_f16` flush only their inputs. The native f16x2 add and multiply is not used while results are flushed. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3, over 19,192 to 162,204 lanes per instruction with op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants and products and sums at the 2^-14 boundary. -- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. +- [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` and the image, buffer, global and flat `fmin/fmax/fcmpswap` flush both operands by the field, see the float atomics entries. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. @@ -141,7 +141,8 @@ - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view) are sampled in the shader, the way color comparisons are emulated: `image_sample` of a 2D or 2D array view fetches the one or four texels at LOD 0, decodes them and blends them, for point or bilinear filtering with equal minification and magnification filters and wrap or clamp-to-edge addressing. A shader gets these sample modes only once a converted descriptor reaches one of its sampled images: that capture throws `ConvertedSampleRequired`, the driver records the program and prepares it again with `GuestContext::convertedSamples`, so such a shader is prepared twice per run and its first capture is repeated. The record lives with the registered program and is not kept across runs. Bilinear weights are rounded to the hardware's 8-bit grid (`roundEven(fraction * 256) / 256`, ties to even), as measured with `image_sample_lz` on RDNA2 (gfx1035) for 10_11_11 UNORM texels by adriadam10 in #5958; point sampling, decoding, swizzles and wrap and clamp-to-edge addressing matched the same measurement. A sample that is not `_lz` needs an image with one mip level; gathers, LOD queries, clamp-to-border and mirrored addressing, other filters, min/max reduction, unnormalized coordinates, images selected at run time, 16-bit data and a DST_SEL of the missing W channel throw. - [Converted unorm image loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. -- [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. The same bit rule serves the buffer float atomics (`AtomicFloatBits`). Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. +- [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. +- [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. - [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). From 06c1c0300f7bf1f8646d23f1b84ea618ff62fc43 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Wed, 21 Oct 2026 10:54:59 +0700 Subject: [PATCH 099/318] test(agc): run the global float atomic modes case once, with one op per width The global kind of agc_driver_memory_float_atomic_modes compiled three shaders with six global float atomics each, and llvmpipe needs minutes to compile them cold, which pushed the test past the CI timeout. The global kind now runs FLOAT_MODE 0x00 only, where both widths flush, and only global_atomic_fmin and global_atomic_fmin_x2; their stored value depends on the flush in 13 of the 32 rows at each width. Buffer and image keep all three operations in all six modes. With the global flush turned off, the global case still fails. Co-Authored-By: Claude --- .../execution/MemoryFloatAtomicModes.cpp | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp index aa4f0afb67..572e59d66f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/MemoryFloatAtomicModes.cpp @@ -65,22 +65,14 @@ alignas(256) constexpr std::array ImageCode{ 0xe0701014u, 0x8001120cu, 0xbf810000u, }; -alignas(256) constexpr std::array GlobalCode{ +alignas(256) constexpr std::array GlobalCode{ 0x160200b0u, 0x34180087u, 0xe0381000u, 0x80000201u, 0xe0381010u, 0x80000601u, 0xe0341020u, 0x80000a01u, 0xbf8c3f70u, 0x34340082u, 0xd70f6a1au, 0x00023408u, 0x7e360209u, 0x50363680u, 0x34380083u, 0xd70f6a1cu, 0x00023808u, 0x7e3a0209u, 0x503a3a80u, 0xdc708000u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xdcfd8000u, 0x137d101au, 0xbf8c3f70u, 0xdc318000u, 0x127d001au, 0xbf8c3f70u, 0xe0701000u, 0x8001130cu, - 0xe0701004u, 0x8001120cu, 0xdc708080u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xdd018080u, - 0x137d101au, 0xbf8c3f70u, 0xdc318080u, 0x127d001au, 0xbf8c3f70u, 0xe0701008u, 0x8001130cu, 0xe070100cu, - 0x8001120cu, 0xdc708100u, 0x007d021au, 0xbbfd0000u, 0x7e200303u, 0x7e220304u, 0xdcf98100u, 0x137d101au, - 0xbf8c3f70u, 0xdc318100u, 0x127d001au, 0xbf8c3f70u, 0xe0701010u, 0x8001130cu, 0xe0701014u, 0x8001120cu, - 0xdc748200u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, 0xdd7d8200u, - 0x207d141cu, 0xbf8c3f70u, 0xdc358200u, 0x187d001cu, 0xbf8c3f70u, 0xe0741018u, 0x8001200cu, 0xe0741020u, - 0x8001180cu, 0xdc748300u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, 0x7e2e030bu, - 0xdd818300u, 0x207d141cu, 0xbf8c3f70u, 0xdc358300u, 0x187d001cu, 0xbf8c3f70u, 0xe0741028u, 0x8001200cu, - 0xe0741030u, 0x8001180cu, 0xdc748400u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, - 0x7e2e030bu, 0xdd798400u, 0x207d141cu, 0xbf8c3f70u, 0xdc358400u, 0x187d001cu, 0xbf8c3f70u, 0xe0741038u, - 0x8001200cu, 0xe0741040u, 0x8001180cu, 0xbf810000u, + 0xe0701004u, 0x8001120cu, 0xdc748200u, 0x007d061cu, 0xbbfd0000u, 0x7e280308u, 0x7e2a0309u, 0x7e2c030au, + 0x7e2e030bu, 0xdd7d8200u, 0x207d141cu, 0xbf8c3f70u, 0xdc358200u, 0x187d001cu, 0xbf8c3f70u, 0xe0741018u, + 0x8001200cu, 0xe0741020u, 0x8001180cu, 0xbf810000u, }; constexpr std::uint32_t Rows32[Threads][3] = { @@ -393,15 +385,16 @@ void Run(AgcDriver::VulkanDevice& device, Kind kind, GuestBlock& guest, const st void Check(Kind kind, const char* mode, bool flush32, bool flush64) { const auto where = std::string("memory float atomic modes: ") + KindName(kind) + ", " + mode + ", lane "; + const std::uint32_t ops = kind == Kind::Global ? 1u : 3u; for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t* out = &Output[tid * Results]; - for (std::uint32_t op = 0; op < 3u; ++op) { + for (std::uint32_t op = 0; op < ops; ++op) { const std::uint32_t expected = flush32 ? Flushed32[tid][op] : Kept32[tid][op]; Require(out[2u * op] == Rows32[tid][0], where + std::to_string(tid) + " " + Names[op] + " returned " + Hex(out[2u * op], 8) + ", expected " + Hex(Rows32[tid][0], 8)); Require(out[2u * op + 1u] == expected, where + std::to_string(tid) + " " + Names[op] + " stored " + Hex(out[2u * op + 1u], 8) + ", expected " + Hex(expected, 8)); } if (kind == Kind::Image) continue; - for (std::uint32_t op = 0; op < 3u; ++op) { + for (std::uint32_t op = 0; op < ops; ++op) { const std::uint64_t returned = out[6u + 4u * op] | (static_cast(out[7u + 4u * op]) << 32u); const std::uint64_t stored = out[8u + 4u * op] | (static_cast(out[9u + 4u * op]) << 32u); const std::uint64_t expected = flush64 ? Flushed64[tid][op] : Kept64[tid][op]; @@ -428,17 +421,19 @@ int main() { const char* name; bool flush32; bool flush64; + bool global; }; const std::array modes{{ - {std::nullopt, "no float mode", true, false}, - {ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, "FLOAT_MODE 0xc0", true, false}, - {ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, "FLOAT_MODE 0xf0", false, false}, - {ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}, "FLOAT_MODE 0x00", true, true}, - {ShaderRecompiler::ShaderFloatMode{0x50u, true, false, false}, "FLOAT_MODE 0x50", false, false}, - {ShaderRecompiler::ShaderFloatMode{0xa0u, true, false, false}, "FLOAT_MODE 0xa0", true, true}, + {std::nullopt, "no float mode", true, false, false}, + {ShaderRecompiler::ShaderFloatMode{0xc0u, true, false, false}, "FLOAT_MODE 0xc0", true, false, false}, + {ShaderRecompiler::ShaderFloatMode{0xf0u, true, false, false}, "FLOAT_MODE 0xf0", false, false, false}, + {ShaderRecompiler::ShaderFloatMode{0x00u, true, false, false}, "FLOAT_MODE 0x00", true, true, true}, + {ShaderRecompiler::ShaderFloatMode{0x50u, true, false, false}, "FLOAT_MODE 0x50", false, false, false}, + {ShaderRecompiler::ShaderFloatMode{0xa0u, true, false, false}, "FLOAT_MODE 0xa0", true, true, false}, }}; for (const auto kind : {Kind::Buffer, Kind::Image, Kind::Global}) { for (const auto& mode : modes) { + if (kind == Kind::Global && !mode.global) continue; Run(*device, kind, guest, mode.floatMode); Check(kind, mode.name, mode.flush32, mode.flush64); } From 48549d1a9c6ca5a6f0ea48ca0908e924c9ec9831 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 05:44:42 +0000 Subject: [PATCH 100/318] perf(agc): resolve a committed shader ABI without the preparation lock Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/ShaderPreparation.hpp | 1 + .../src/Driver/Shaders/ShaderRegistry.cpp | 45 ++++++++++++++++-- .../tests/execution/PreparedPublication.cpp | 29 ++++++++++++ .../tests/execution/PreparedShaders.cpp | 46 +++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp index 98fd3cbadd..70d81c98a5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp @@ -16,6 +16,7 @@ public: ShaderPreparationTransaction& operator=(const ShaderPreparationTransaction&) = delete; PreparedShaderState& Edit(const ShaderSnapshot& snapshot); const PreparedShaderState& Read(const ShaderSnapshot& snapshot) const; + bool Tracks(const ShaderSnapshot& snapshot) const; void Hold(); void Commit(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index b69bccd8fe..0a44eb264a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -220,6 +220,24 @@ bool CommittedAbi(const ShaderSnapshot& snapshot, std::span return std::ranges::any_of(snapshot.prepared->registeredAbis, [&](const auto& abi) { return std::ranges::equal(abi, key); }); } +std::optional>> CommittedGraphicsAbi(const ShaderSnapshot& owner, std::span key, const std::shared_ptr& registry) { + std::unique_lock lock(owner.prepared->mutex); + AwaitRegisteredPreparation(*owner.prepared, lock); + const auto& abis = owner.prepared->graphicsAbis; + const auto found = std::ranges::find_if(abis, [&](const auto& abi) { return std::ranges::equal(abi.key, key) && abi.registry.lock() == registry; }); + if (found == abis.end()) return std::nullopt; + return found->stages; +} + +bool CommittedLink(const ShaderSnapshot& front, const std::shared_ptr& fragment, std::uint32_t primitiveType) { + if (primitiveType == 7 || primitiveType == 17) return false; + std::unique_lock lock(front.prepared->mutex); + AwaitRegisteredPreparation(*front.prepared, lock); + const auto& fragments = front.prepared->fragments; + if (front.prepared->rectangleRequested || std::ranges::any_of(fragments, [](const auto& entry) { return entry.expired(); })) return false; + return fragment == nullptr || std::ranges::any_of(fragments, [&](const auto& entry) { return entry.lock() == fragment; }); +} + } bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset) { @@ -351,6 +369,10 @@ const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapsh return changes.emplace(snapshot.prepared.get(), State::Change{snapshot.prepared, *snapshot.prepared, false, snapshot.prepared->entries.size(), snapshot.prepared->registeredAbis.size()}).first->second.prepared; } +bool ShaderPreparationTransaction::Tracks(const ShaderSnapshot& snapshot) const { + return root->changes.contains(snapshot.prepared.get()); +} + void ShaderPreparationTransaction::Hold() { require(!committed, "shader preparation transaction is already committed"); root->Hold(); @@ -1531,14 +1553,23 @@ void Driver::ResolveGraphicsStagesAbi(std::span stages, std auto& key = HostThreadLocal, GraphicsAbiKeyStorage>(); BuildRegisteredAbiKey(state, *localDevice, key); const auto primitiveType = state.userConfig.find(0x242u); - for (const auto& abi : transaction.Read(*owner).graphicsAbis) { - if (abi.key != key || abi.registry.lock() != registry) continue; - for (const auto& stage : abi.stages) { + const auto resolveStages = [&](const std::vector>& stages) { + for (const auto& stage : stages) { const auto snapshot = stage.lock(); require(snapshot != nullptr, "prepared graphics ABI lost a registered stage"); if (snapshot->type != 1) ResolvePreparedGraphics(*snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target(), true); } transaction.Commit(); + }; + if (!transaction.Tracks(*owner)) { + if (const auto committed = CommittedGraphicsAbi(*owner, key, registry)) { + resolveStages(*committed); + return; + } + } + for (const auto& abi : transaction.Read(*owner).graphicsAbis) { + if (abi.key != key || abi.registry.lock() != registry) continue; + resolveStages(abi.stages); return; } DrawDecode decoded{}; @@ -1624,6 +1655,10 @@ void Driver::ResolveShaderAbi(const Shader* shader, std::span{} : prepare(); const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis; if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) { @@ -1709,6 +1744,10 @@ void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr< return; } } + if (!transaction.Tracks(front) && CommittedLink(front, fragment, primitiveType)) { + transaction.Commit(); + return; + } std::vector> fragments; { const auto& current = transaction.Read(front); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp index beaed11d60..1273ff05c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedPublication.cpp @@ -371,6 +371,34 @@ void TransactionLock() { Require(excluded, "a transaction ran beside one that had read prepared state"); } +void CommittedLinks() { + ShaderSnapshot front{}; + auto pixel = std::make_shared(); + ResolvePreparedGraphics(front, pixel, 0, {}); + Require(front.prepared->fragments.size() == 1 && front.prepared->fragments.front().lock() == pixel, "a resolved link was not published"); + ShaderSnapshot unrelated{}; + auto other = std::make_shared(); + std::optional open(std::in_place); + static_cast(open->Read(unrelated)); + auto repeated = std::async(std::launch::async, [&] { ResolvePreparedGraphics(front, pixel, 0, {}); }); + const bool unblocked = repeated.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + auto linking = std::async(std::launch::async, [&] { ResolvePreparedGraphics(front, other, 0, {}); }); + const bool excluded = linking.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout; + open->Commit(); + open.reset(); + repeated.get(); + linking.get(); + Require(unblocked, "resolving a link the shader already had waited for another thread's transaction"); + Require(excluded && front.prepared->fragments.size() == 2, "a new link was published beside a transaction that had read prepared state"); + { + ShaderPreparationTransaction transaction; + transaction.Edit(front).fragments.clear(); + ResolvePreparedGraphics(front, pixel, 0, {}); + transaction.Commit(); + } + Require(front.prepared->fragments.size() == 1 && front.prepared->fragments.front().lock() == pixel, "a link resolved in a transaction that had changed the shader was checked against the published links"); +} + void PreparationPriority(bool beforeSubmit) { const auto workers = std::max(2u, std::thread::hardware_concurrency() / 2u); std::barrier started(static_cast(workers + 1u)); @@ -415,6 +443,7 @@ int main() { RegistryVersions(); FusedHullHeaderSelection(); TransactionLock(); + CommittedLinks(); PreparationPriority(false); PreparationPriority(true); auto device = OpenVulkanTestDevice(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index 734a7c6825..b72d48eb52 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1198,6 +1198,51 @@ void AbiDeferredRectanglePairs() { ExpectFailure([] { AgcDriverShutdown_nid_postfix(); }, "custom per-vertex interpolation is unsupported"); } +void CommittedAbis() { + static AbiHeader front; + static AbiHeader pixel; + DescribeFront(front); + DescribeAbi(pixel, PassthroughPixelCode, 1, {{0x8, ProgramLow(PassthroughPixelCode.data())}, {0x9, ProgramHigh(PassthroughPixelCode.data())}, {0xa, 0}, {0xb, 0}}, {{0x1b6, 1}, {0x1b3, 2}, {0x1b4, 2}, {0x191, 0}, {0x1c5, 4}}); + for (auto* header : {&front, &pixel}) AgcDriverRegisterShader_nid_postfix(&header->shader); + const std::array stages{&front.shader, &pixel.shader}; + const std::array primitive{{{0x242, 4}}}; + const std::array interpolant{{{0x191, 0x20}}}; + const auto resolve = [&] { + AgcDriver::ShaderPreparationScope scope({&front.shader, &pixel.shader}); + AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, {}, {}); + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, {}, primitive); + AgcDriverResolveGraphicsAbi_nid_postfix(&front.shader, &pixel.shader, 0); + scope.Commit(); + }; + const auto resolveOther = [&] { + AgcDriver::ShaderPreparationScope scope({&pixel.shader}); + AgcDriverResolveShaderAbi_nid_postfix(&pixel.shader, interpolant, {}); + scope.Commit(); + }; + resolve(); + const auto before = ShaderRecompiler::FrontEndBuilds(); + bool unblocked = false; + bool excluded = false; + std::uint64_t repeatedBuilds = 0; + std::future repeated; + std::future other; + { + AgcDriver::DriverDetail::ShaderSnapshot unrelated{}; + AgcDriver::DriverDetail::ShaderPreparationTransaction open; + static_cast(open.Read(unrelated)); + repeated = std::async(std::launch::async, resolve); + unblocked = repeated.wait_for(std::chrono::seconds(10)) == std::future_status::ready; + repeatedBuilds = ShaderRecompiler::FrontEndBuilds() - before; + other = std::async(std::launch::async, resolveOther); + excluded = other.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout; + open.Commit(); + } + repeated.get(); + other.get(); + Require(unblocked && repeatedBuilds == 0, "resolving ABIs the shaders already had waited for another thread's shader preparation, or prepared them again"); + Require(excluded, "a new ABI was published beside a shader preparation that had read prepared state"); +} + void RelocatedHeaders() { alignas(256) static const std::array code{0x7e0002ffu, 0, 0x7e0202ffu, 0, 0x7e0402ffu, 0, 0x7e0602ffu, 0x3f800000u, 0xf80008cfu, 0x03020100u, 0xbf810000u}; struct Header { @@ -1318,6 +1363,7 @@ int main(int argc, char** argv) { device.reset(); SharedCodeHeaders(); RelocatedHeaders(); + CommittedAbis(); RegistrationWithoutSpecials(); Registration(argc == 2); std::cout << "prepared shader and transactional registration tests passed\n"; From 9f32653dcf6564b397a9301af1f27e2bd08f5fc3 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:21:52 +0530 Subject: [PATCH 101/318] refactor(libSceZlib): track results by request ID Keep finished requests in a map keyed by request ID and their IDs in a completion queue instead of a single optional result. This prepares for several outstanding requests. sceZlibWaitForDone now pops the completion queue, so it returns each finished request once and no longer depends on whether its result was read, as in shadPS4's done queue. A second wait for the same request throws as before when nothing else is pending. --- core/libs/prx/libSceZlib/Export.cpp | 42 ++++++++++++++++++----------- core/libs/tests/GuestZlib.cpp | 1 + 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 22135e465c..497edffc2e 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -1,10 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "zlib.h" +#include #include +#include #include #include -#include #include +#include namespace { @@ -16,7 +18,6 @@ constexpr std::int32_t NO_SPACE = static_cast(0x8112001C); constexpr std::int32_t FATAL = static_cast(0x811200FF); struct Result { - std::uint64_t id; std::uint32_t produced; std::int32_t status; bool retrieved = false; @@ -25,7 +26,12 @@ struct Result { std::mutex serviceMutex; bool initialized = false; std::uint64_t nextId = 1; -std::optional result; +std::unordered_map results; +std::deque completed; + +bool hasUnreadResult() { + return std::any_of(results.begin(), results.end(), [](const auto& entry) { return !entry.second.retrieved; }); +} } // namespace @@ -42,8 +48,9 @@ std::int32_t APS5_VABI sceZlibInitialize(const void* buffer, std::uint64_t lengt std::int32_t APS5_VABI sceZlibFinalize() { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (result && !result->retrieved) throw std::runtime_error("sceZlibFinalize: outstanding result semantics unknown"); - result.reset(); + if (hasUnreadResult()) throw std::runtime_error("sceZlibFinalize: outstanding result semantics unknown"); + results.clear(); + completed.clear(); initialized = false; return 0; } @@ -56,7 +63,7 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe if (!source || !sourceLength || !destination || !requestId || !destinationLength || destinationLength > 65536 || reinterpret_cast(destination) % 2048 != 0) throw std::runtime_error("sceZlibInflate: invalid buffer or size"); - if (result && !result->retrieved) throw std::runtime_error("sceZlibInflate: multiple outstanding requests unsupported"); + if (hasUnreadResult()) throw std::runtime_error("sceZlibInflate: multiple outstanding requests unsupported"); if (nextId == std::numeric_limits::max()) throw std::runtime_error("sceZlibInflate: request IDs exhausted"); z_stream stream{}; stream.next_in = const_cast(static_cast(source)); @@ -70,8 +77,10 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe (decoded == Z_BUF_ERROR && stream.avail_out == 0 && stream.avail_in != 0 ? NO_SPACE : FATAL); const int ended = inflateEnd(&stream); if (ended != Z_OK) throw std::runtime_error("sceZlibInflate: decoder cleanup failed"); - result = Result{nextId++, produced, status}; - *requestId = result->id; + const std::uint64_t id = nextId++; + results[id] = Result{produced, status}; + completed.push_back(id); + *requestId = id; return 0; } @@ -79,8 +88,9 @@ std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_ std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; if (timeout) throw std::runtime_error("sceZlibWaitForDone: timeout semantics unknown"); - if (!requestId || !result || result->retrieved) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); - *requestId = result->id; + if (!requestId || completed.empty()) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); + *requestId = completed.front(); + completed.pop_front(); return 0; } @@ -88,12 +98,14 @@ std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* std::int32_t* status) { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (!destinationLength || !status || !result || result->id != requestId) + const auto found = results.find(requestId); + if (!destinationLength || !status || found == results.end()) throw std::runtime_error("sceZlibGetResult: invalid request or output"); - if (result->retrieved) throw std::runtime_error("sceZlibGetResult: repeated retrieval semantics unknown"); - *destinationLength = result->produced; - *status = result->status; - result->retrieved = true; + Result& result = found->second; + if (result.retrieved) throw std::runtime_error("sceZlibGetResult: repeated retrieval semantics unknown"); + *destinationLength = result.produced; + *status = result.status; + result.retrieved = true; return 0; } diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 50a9688420..6ea8596699 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -52,6 +52,7 @@ int main() { std::uint64_t completed; Require(sceZlibWaitForDone(&completed, nullptr) == 0); Require(completed == id); + RequireThrows([&] { sceZlibWaitForDone(&completed, nullptr); }); RequireThrows([&] { sceZlibGetResult(id + 1, &produced, &status); }); Require(sceZlibGetResult(id, &produced, &status) == 0); Require(status == expectedStatus && produced <= capacity); From 7322375923409d40baa4d3281810362245456182 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:22:32 +0530 Subject: [PATCH 102/318] feat(libSceZlib): accept requests while earlier results are unread sceZlibInflate threw when an earlier request's result had not been read. Accept the request instead: every finished request keeps its result, and sceZlibWaitForDone returns the finished IDs in submission order, as shadPS4's done queue does. The PS5 completion order is unverified and recorded in the technical debt entry. --- core/libs/prx/libSceZlib/Export.cpp | 1 - core/libs/tests/GuestZlib.cpp | 16 +++++++++++++++- docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 497edffc2e..6c284515e7 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -63,7 +63,6 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe if (!source || !sourceLength || !destination || !requestId || !destinationLength || destinationLength > 65536 || reinterpret_cast(destination) % 2048 != 0) throw std::runtime_error("sceZlibInflate: invalid buffer or size"); - if (hasUnreadResult()) throw std::runtime_error("sceZlibInflate: multiple outstanding requests unsupported"); if (nextId == std::numeric_limits::max()) throw std::runtime_error("sceZlibInflate: request IDs exhausted"); z_stream stream{}; stream.next_in = const_cast(static_cast(source)); diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 6ea8596699..1b4dd2369a 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -45,7 +45,6 @@ int main() { Require(sceZlibInflate(input, inputLength, output, capacity, &id) == 0); Require(id > previous); previous = id; - RequireThrows([&] { sceZlibInflate(input, inputLength, output, capacity, &id); }); RequireThrows([&] { sceZlibFinalize(); }); std::uint32_t timeout = 0; RequireThrows([&] { sceZlibWaitForDone(&id, &timeout); }); @@ -71,6 +70,21 @@ int main() { decode(compressed.data(), compressed.size() - 1, 65536, static_cast(0x811200FF)); decode(compressed.data(), compressed.size() - 4, expectedLength, static_cast(0x811200FF)); decode(compressed.data(), compressed.size() - 1, expectedLength, static_cast(0x811200FF)); + alignas(2048) std::array batch{}; + std::array ids{}; + for (std::size_t index = 0; index < ids.size(); ++index) + Require(sceZlibInflate(compressed.data(), compressed.size(), batch.data() + index * 8192, 8192, &ids[index]) == 0); + Require(ids[0] < ids[1] && ids[1] < ids[2]); + for (std::uint64_t expected : ids) { + std::uint64_t completed = 0; + Require(sceZlibWaitForDone(&completed, nullptr) == 0); + Require(completed == expected); + } + for (std::size_t index = 0; index < ids.size(); ++index) { + Require(sceZlibGetResult(ids[index], &produced, &status) == 0); + Require(status == 0 && produced == expectedLength); + Require(std::memcmp(batch.data() + index * 8192, text, sizeof(text) - 1) == 0); + } Require(sceZlibFinalize() == 0); Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120032)); std::array scratch{}; diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index 5d289d575b..2acdecf034 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports one outstanding request, null-timeout waits and one result retrieval. Timed waits, multiple outstanding requests, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits and one result retrieval. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. Timed waits, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. From a5279134a199dfeab58e42963d13e675dc1aa8d6 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:22:53 +0530 Subject: [PATCH 103/318] feat(libSceZlib): allow repeated result retrieval sceZlibGetResult threw on a second call for the same request and on an ID it did not hold. Return the same length and status on every call, and 0x81120002 (not found) for an unknown ID, as shadPS4 does. Null output pointers still throw. --- core/libs/prx/libSceZlib/Export.cpp | 6 +++--- core/libs/tests/GuestZlib.cpp | 8 ++++++-- docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 6c284515e7..b0943dd9cb 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -12,6 +12,7 @@ namespace { static_assert(sizeof(std::uint32_t) == 4 && sizeof(std::uint64_t) == 8 && sizeof(std::int32_t) == 4); +constexpr std::int32_t NOT_FOUND = static_cast(0x81120002); constexpr std::int32_t NOT_INITIALIZED = static_cast(0x81120032); constexpr std::int32_t ALREADY_INITIALIZED = static_cast(0x81120033); constexpr std::int32_t NO_SPACE = static_cast(0x8112001C); @@ -97,11 +98,10 @@ std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* std::int32_t* status) { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; + if (!destinationLength || !status) throw std::runtime_error("sceZlibGetResult: invalid output"); const auto found = results.find(requestId); - if (!destinationLength || !status || found == results.end()) - throw std::runtime_error("sceZlibGetResult: invalid request or output"); + if (found == results.end()) return NOT_FOUND; Result& result = found->second; - if (result.retrieved) throw std::runtime_error("sceZlibGetResult: repeated retrieval semantics unknown"); *destinationLength = result.produced; *status = result.status; result.retrieved = true; diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 1b4dd2369a..1f5c9a3dda 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -52,10 +52,14 @@ int main() { Require(sceZlibWaitForDone(&completed, nullptr) == 0); Require(completed == id); RequireThrows([&] { sceZlibWaitForDone(&completed, nullptr); }); - RequireThrows([&] { sceZlibGetResult(id + 1, &produced, &status); }); + Require(sceZlibGetResult(id + 1, &produced, &status) == static_cast(0x81120002)); + RequireThrows([&] { sceZlibGetResult(id, nullptr, &status); }); Require(sceZlibGetResult(id, &produced, &status) == 0); Require(status == expectedStatus && produced <= capacity); - RequireThrows([&] { sceZlibGetResult(id, &produced, &status); }); + std::uint32_t producedAgain = 0; + std::int32_t statusAgain = 0; + Require(sceZlibGetResult(id, &producedAgain, &statusAgain) == 0); + Require(producedAgain == produced && statusAgain == status); Require(std::all_of(storage.begin(), storage.begin() + 2048, [](auto byte) { return byte == 0xA5; })); Require(std::all_of(storage.begin() + 2048 + capacity, storage.end(), [](auto byte) { return byte == 0xA5; })); }; diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index 2acdecf034..6a16242558 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits and one result retrieval. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. Timed waits, repeated retrieval and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Timed waits and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. From 18e406653077bc4c1ba306f97745a1652a7dc501 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:23:19 +0530 Subject: [PATCH 104/318] feat(libSceZlib): discard unread results on finalize sceZlibFinalize threw while a finished request's result was unread. Discard the stored results and the finished IDs not yet returned by sceZlibWaitForDone instead. shadPS4 also returns success here and never reports the abandoned requests; its next initialization clears them. An ID from before finalization reports 0x81120002 afterwards. --- core/libs/prx/libSceZlib/Export.cpp | 13 ++----------- core/libs/tests/GuestZlib.cpp | 5 ++++- docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index b0943dd9cb..743dd271b1 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -1,6 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "zlib.h" -#include #include #include #include @@ -21,7 +20,6 @@ constexpr std::int32_t FATAL = static_cast(0x811200FF); struct Result { std::uint32_t produced; std::int32_t status; - bool retrieved = false; }; std::mutex serviceMutex; @@ -30,10 +28,6 @@ std::uint64_t nextId = 1; std::unordered_map results; std::deque completed; -bool hasUnreadResult() { - return std::any_of(results.begin(), results.end(), [](const auto& entry) { return !entry.second.retrieved; }); -} - } // namespace extern "C" { @@ -49,7 +43,6 @@ std::int32_t APS5_VABI sceZlibInitialize(const void* buffer, std::uint64_t lengt std::int32_t APS5_VABI sceZlibFinalize() { std::lock_guard lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (hasUnreadResult()) throw std::runtime_error("sceZlibFinalize: outstanding result semantics unknown"); results.clear(); completed.clear(); initialized = false; @@ -101,10 +94,8 @@ std::int32_t APS5_VABI sceZlibGetResult(std::uint64_t requestId, std::uint32_t* if (!destinationLength || !status) throw std::runtime_error("sceZlibGetResult: invalid output"); const auto found = results.find(requestId); if (found == results.end()) return NOT_FOUND; - Result& result = found->second; - *destinationLength = result.produced; - *status = result.status; - result.retrieved = true; + *destinationLength = found->second.produced; + *status = found->second.status; return 0; } diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 1f5c9a3dda..b847e3dd70 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -45,7 +45,6 @@ int main() { Require(sceZlibInflate(input, inputLength, output, capacity, &id) == 0); Require(id > previous); previous = id; - RequireThrows([&] { sceZlibFinalize(); }); std::uint32_t timeout = 0; RequireThrows([&] { sceZlibWaitForDone(&id, &timeout); }); std::uint64_t completed; @@ -89,6 +88,10 @@ int main() { Require(status == 0 && produced == expectedLength); Require(std::memcmp(batch.data() + index * 8192, text, sizeof(text) - 1) == 0); } + Require(sceZlibInflate(compressed.data(), compressed.size(), output, expectedLength, &id) == 0); + Require(sceZlibFinalize() == 0); + Require(sceZlibInitialize(nullptr, 0) == 0); + Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120002)); Require(sceZlibFinalize() == 0); Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120032)); std::array scratch{}; diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index 6a16242558..d56d88c21f 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Timed waits and finalization with an unread result throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. Timed waits throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. From 4020c844cc5b079506914e218f7a6ad68ebaaeea Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:24:05 +0530 Subject: [PATCH 105/318] feat(libSceZlib): support timed waits in sceZlibWaitForDone sceZlibWaitForDone threw for any timeout and when no finished request was pending. Wait on a condition variable that sceZlibInflate signals: a null timeout blocks until a request finishes, and a timeout returns 0x81120027 (timed out) on expiry, as in shadPS4. The timeout is read as microseconds, as SharpProspero documents for the PS5; shadPS4 reads milliseconds. --- core/libs/prx/libSceZlib/Export.cpp | 13 ++++++++++--- core/libs/tests/GuestZlib.cpp | 22 +++++++++++++++++++--- docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 743dd271b1..1bb8114ec7 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "zlib.h" +#include +#include #include #include #include @@ -14,6 +16,7 @@ static_assert(sizeof(std::uint32_t) == 4 && sizeof(std::uint64_t) == 8 && sizeof constexpr std::int32_t NOT_FOUND = static_cast(0x81120002); constexpr std::int32_t NOT_INITIALIZED = static_cast(0x81120032); constexpr std::int32_t ALREADY_INITIALIZED = static_cast(0x81120033); +constexpr std::int32_t TIMED_OUT = static_cast(0x81120027); constexpr std::int32_t NO_SPACE = static_cast(0x8112001C); constexpr std::int32_t FATAL = static_cast(0x811200FF); @@ -27,6 +30,7 @@ bool initialized = false; std::uint64_t nextId = 1; std::unordered_map results; std::deque completed; +std::condition_variable completion; } // namespace @@ -73,15 +77,18 @@ std::int32_t APS5_VABI sceZlibInflate(const void* source, std::uint32_t sourceLe const std::uint64_t id = nextId++; results[id] = Result{produced, status}; completed.push_back(id); + completion.notify_one(); *requestId = id; return 0; } std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_t* timeout) { - std::lock_guard lock(serviceMutex); + std::unique_lock lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; - if (timeout) throw std::runtime_error("sceZlibWaitForDone: timeout semantics unknown"); - if (!requestId || completed.empty()) throw std::runtime_error("sceZlibWaitForDone: no outstanding request"); + if (!requestId) throw std::runtime_error("sceZlibWaitForDone: invalid output"); + const auto finished = [] { return !completed.empty(); }; + if (!timeout) completion.wait(lock, finished); + else if (!completion.wait_for(lock, std::chrono::microseconds(*timeout), finished)) return TIMED_OUT; *requestId = completed.front(); completed.pop_front(); return 0; diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index b847e3dd70..4422d865ea 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -1,10 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include #include +#include extern "C" { std::int32_t APS5_VABI sceZlibInitialize(const void*, std::uint64_t); @@ -45,12 +47,11 @@ int main() { Require(sceZlibInflate(input, inputLength, output, capacity, &id) == 0); Require(id > previous); previous = id; - std::uint32_t timeout = 0; - RequireThrows([&] { sceZlibWaitForDone(&id, &timeout); }); std::uint64_t completed; Require(sceZlibWaitForDone(&completed, nullptr) == 0); Require(completed == id); - RequireThrows([&] { sceZlibWaitForDone(&completed, nullptr); }); + std::uint32_t timeout = 0; + Require(sceZlibWaitForDone(&completed, &timeout) == static_cast(0x81120027)); Require(sceZlibGetResult(id + 1, &produced, &status) == static_cast(0x81120002)); RequireThrows([&] { sceZlibGetResult(id, nullptr, &status); }); Require(sceZlibGetResult(id, &produced, &status) == 0); @@ -88,6 +89,21 @@ int main() { Require(status == 0 && produced == expectedLength); Require(std::memcmp(batch.data() + index * 8192, text, sizeof(text) - 1) == 0); } + std::uint64_t completed = 0; + std::uint32_t timeout = 0; + Require(sceZlibInflate(compressed.data(), compressed.size(), output, expectedLength, &id) == 0); + Require(sceZlibWaitForDone(&completed, &timeout) == 0); + Require(completed == id); + timeout = 20000; + const auto start = std::chrono::steady_clock::now(); + Require(sceZlibWaitForDone(&completed, &timeout) == static_cast(0x81120027)); + const auto waited = std::chrono::steady_clock::now() - start; + Require(waited >= std::chrono::microseconds(20000) && waited < std::chrono::seconds(1)); + std::thread waiter([&] { Require(sceZlibWaitForDone(&completed, nullptr) == 0); }); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(sceZlibInflate(compressed.data(), compressed.size(), output, expectedLength, &id) == 0); + waiter.join(); + Require(completed == id); Require(sceZlibInflate(compressed.data(), compressed.size(), output, expectedLength, &id) == 0); Require(sceZlibFinalize() == 0); Require(sceZlibInitialize(nullptr, 0) == 0); diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index d56d88c21f..19a43d2761 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission and supports null-timeout waits. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. Timed waits throw. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. `sceZlibWaitForDone` with a null timeout blocks until a request finishes; a timeout is read as microseconds, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs) documents (shadPS4 reads milliseconds), and expiry returns `0x81120027` as in shadPS4. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. From 729723a60f1414fbd36d9280b90c9fb614a018a5 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:24:48 +0530 Subject: [PATCH 106/318] feat(libSceZlib): throw when finalize interrupts a waiter A thread blocked in sceZlibWaitForDone was never woken by sceZlibFinalize, so it waited for its whole timeout or forever. Finalization now wakes every waiter, and a waiter that sees the service finalized throws, since no source says what the console returns. shadPS4 leaves such a waiter blocked. --- core/libs/prx/libSceZlib/Export.cpp | 7 ++++++- core/libs/tests/GuestZlib.cpp | 14 ++++++++++++++ docs/dev/debt/libSceZlib.md | 2 +- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceZlib/Export.cpp b/core/libs/prx/libSceZlib/Export.cpp index 1bb8114ec7..1f43d71b4f 100644 --- a/core/libs/prx/libSceZlib/Export.cpp +++ b/core/libs/prx/libSceZlib/Export.cpp @@ -28,6 +28,7 @@ struct Result { std::mutex serviceMutex; bool initialized = false; std::uint64_t nextId = 1; +std::uint64_t session = 0; std::unordered_map results; std::deque completed; std::condition_variable completion; @@ -50,6 +51,8 @@ std::int32_t APS5_VABI sceZlibFinalize() { results.clear(); completed.clear(); initialized = false; + ++session; + completion.notify_all(); return 0; } @@ -86,9 +89,11 @@ std::int32_t APS5_VABI sceZlibWaitForDone(std::uint64_t* requestId, std::uint32_ std::unique_lock lock(serviceMutex); if (!initialized) return NOT_INITIALIZED; if (!requestId) throw std::runtime_error("sceZlibWaitForDone: invalid output"); - const auto finished = [] { return !completed.empty(); }; + const std::uint64_t waitSession = session; + const auto finished = [waitSession] { return !completed.empty() || session != waitSession; }; if (!timeout) completion.wait(lock, finished); else if (!completion.wait_for(lock, std::chrono::microseconds(*timeout), finished)) return TIMED_OUT; + if (session != waitSession) throw std::runtime_error("sceZlibWaitForDone: finalization during a wait semantics unknown"); *requestId = completed.front(); completed.pop_front(); return 0; diff --git a/core/libs/tests/GuestZlib.cpp b/core/libs/tests/GuestZlib.cpp index 4422d865ea..37f445e8ae 100644 --- a/core/libs/tests/GuestZlib.cpp +++ b/core/libs/tests/GuestZlib.cpp @@ -108,7 +108,21 @@ int main() { Require(sceZlibFinalize() == 0); Require(sceZlibInitialize(nullptr, 0) == 0); Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120002)); + bool interrupted = false; + std::thread interruptedWaiter([&] { + std::uint32_t longTimeout = 5000000; + const auto waitStart = std::chrono::steady_clock::now(); + try { + interrupted = sceZlibWaitForDone(&completed, &longTimeout) == static_cast(0x81120032); + } catch (const std::runtime_error&) { + interrupted = true; + } + Require(std::chrono::steady_clock::now() - waitStart < std::chrono::seconds(1)); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); Require(sceZlibFinalize() == 0); + interruptedWaiter.join(); + Require(interrupted); Require(sceZlibGetResult(id, &produced, &status) == static_cast(0x81120032)); std::array scratch{}; Require(sceZlibInitialize(scratch.data(), scratch.size()) == 0); diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index 19a43d2761..fdd75fb074 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. `sceZlibWaitForDone` with a null timeout blocks until a request finishes; a timeout is read as microseconds, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs) documents (shadPS4 reads milliseconds), and expiry returns `0x81120027` as in shadPS4. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. `sceZlibWaitForDone` with a null timeout blocks until a request finishes; a timeout is read as microseconds, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs) documents (shadPS4 reads milliseconds), and expiry returns `0x81120027` as in shadPS4. A wait interrupted by `sceZlibFinalize` throws: shadPS4 leaves such a waiter blocked, and the console's result is unknown. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. From 704e872d36ede3777a1ae765aaf0695091195cf1 Mon Sep 17 00:00:00 2001 From: enfuego27826 Date: Wed, 21 Oct 2026 12:25:29 +0530 Subject: [PATCH 107/318] docs(libSceZlib): state the remaining gaps first Rewrite the technical debt entry to list what is unverified on the PS5 and what still throws, with the source each behaviour follows, and drop the descriptions of behaviour that is no longer a gap. The entry is now under the 1000 character limit. --- docs/dev/debt/libSceZlib.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceZlib.md b/docs/dev/debt/libSceZlib.md index fdd75fb074..758138279b 100644 --- a/docs/dev/debt/libSceZlib.md +++ b/docs/dev/debt/libSceZlib.md @@ -2,4 +2,4 @@ ### Functional -- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) completes RFC1950 decompression during submission. Several outstanding requests are accepted, and `sceZlibWaitForDone` returns each finished request once in submission order, as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp); the PS5 completion order is unverified. `sceZlibGetResult` returns the same result on every call and `0x81120002` for an ID it does not hold, as in shadPS4; it never returns `0x8112000B`, which SharpProspero documents as a result that is not ready, because every request finishes during submission. Results are kept until `sceZlibFinalize`, which discards unread results and finished IDs not yet waited for, as shadPS4's finalization leaves them to be cleared by the next initialization. `sceZlibWaitForDone` with a null timeout blocks until a request finishes; a timeout is read as microseconds, as [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs) documents (shadPS4 reads milliseconds), and expiry returns `0x81120027` as in shadPS4. A wait interrupted by `sceZlibFinalize` throws: shadPS4 leaves such a waiter blocked, and the console's result is unknown. The 64-bit initialization length and 64 KiB output limit with 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs); the PS4 initialization length is 32 bits. shadPS4 also requires the output length to be a multiple of 2 KiB and returns `0x81120016` for invalid arguments; this implementation accepts any length up to 64 KiB (SharpProspero documents only the pointer alignment) and throws on invalid arguments. Scratch memory is not used, and its firmware size/alignment requirements are unknown. The initialization errors follow SharpProspero; malformed/checksum/truncated streams report `0x811200FF` and insufficient output `0x8112001C` as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp). PS5 error precedence, scheduling, completion selection, result consumption and cancellation are unverified. The payload SDK's FreeBSD kernel `z_stream` and host errno are not this library's ABI. +- [libSceZlib](../../../core/libs/prx/libSceZlib/Export.cpp) is unverified on PS5: several outstanding requests in submission order, repeated `sceZlibGetResult`, `0x81120002` for an unknown ID, `0x81120027` on timeout, results dropped by `sceZlibFinalize` and status codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/zlib/zlib.cpp) (PS4); the microsecond timeout, 64 KiB output limit and 2 KiB destination alignment follow [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Compression/Zlib.cs). `0x8112000B` (not ready) is never returned: requests finish at submission. Invalid arguments and a wait interrupted by `sceZlibFinalize` throw; shadPS4 returns `0x81120016` and requires output lengths in 2 KiB multiples. Error precedence and scratch memory needs are unknown. The payload SDK's kernel `z_stream` and errno are not this ABI. From 00f7cb9c2192ea915345aa4d094def42ba48cbe4 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:41:37 +0300 Subject: [PATCH 108/318] fix(shader): restore compaction validation, shift subword loads and gate OpExtInst purity --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 11 +++++ .../tests/execution/BufferFormat.cpp | 16 ++++---- core/shader/recompiler/Recompiler.cpp | 3 ++ .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 7 +--- .../SpirvBackend/src/SpirvSpecialization.cpp | 40 ++++++++++--------- 5 files changed, 47 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 00796cd2b5..d78cb62a9f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -322,6 +322,8 @@ void testPrivateCompositeTableLoadFolding() { const auto input = module.DefineGlobalVariable(typeInputPtr, spv::StorageClassInput); const auto table = module.DefineInitializedGlobalVariable(typeTablePtr, spv::StorageClassPrivate, initial); const auto output = module.DefineGlobalVariable(typePrivatePtr, spv::StorageClassPrivate); + const auto glsl = module.Import("GLSL.std.450"); + const auto customSet = module.Import("NonSemantic.Test"); const auto mainId = module.AllocateId(); const auto lane = module.AllocateId(); const auto rawCond = module.AllocateId(); @@ -339,6 +341,11 @@ void testPrivateCompositeTableLoadFolding() { module.AddFunction(accessOp, typePrivatePtr, writePtr, table, zero); module.AddFunction(spv::OpStore, writePtr, lane); } + const auto liveGlsl = module.AllocateId(); + module.AddFunction(spv::OpExtInst, typeU32, module.AllocateId(), glsl, 37u, lane, zero); + module.AddFunction(spv::OpExtInst, typeU32, liveGlsl, glsl, 37u, lane, one); + module.AddFunction(spv::OpStore, output, liveGlsl); + module.AddFunction(spv::OpExtInst, typeU32, module.AllocateId(), customSet, 1u, lane); module.AddFunction(spv::OpUGreaterThan, typeBool, rawCond, lane, zero); module.AddFunction(spv::OpLogicalOr, typeBool, cond, rawCond, falseVal); module.AddFunction(spv::OpIAdd, typeU32, baseIndex, one, one); @@ -357,12 +364,16 @@ void testPrivateCompositeTableLoadFolding() { check(!foldedCounts.contains(spv::OpLogicalOr), "a right-false OpLogicalOr was not simplified"); check(!foldedCounts.contains(spv::OpSelect), "an identical-arm OpSelect was not simplified"); check(foldedCounts.contains(spv::OpLoad) && foldedCounts.at(spv::OpLoad) == 1u, "a constant-index private composite table load was not folded"); + check(foldedCounts.contains(spv::OpExtInst) && foldedCounts.at(spv::OpExtInst) == 2u, "an unused GLSL.std.450 OpExtInst was not removed or a live/non-GLSL.std.450 OpExtInst was removed"); bool addedThirty = false; + bool keptCustomExtInst = false; for (std::size_t cursor = 5; cursor < folded.size(); cursor += folded[cursor] >> 16u) { if ((folded[cursor] >> 16u) == 0u) break; if ((folded[cursor] & 0xffffu) == spv::OpIAdd && folded[cursor + 4u] == expectedConstant) addedThirty = true; + if ((folded[cursor] & 0xffffu) == spv::OpExtInst && folded[cursor + 4u] == 1u) keptCustomExtInst = true; } check(addedThirty, "a constant-index private composite table load did not fold to its element constant"); + check(keptCustomExtInst, "a non-GLSL.std.450 OpExtInst was not preserved"); const auto [dynamic, unusedDynamic] = build(true, false); static_cast(unusedDynamic); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp index 5a93b53303..7394269e39 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp @@ -173,7 +173,7 @@ void Check() { constexpr std::array SubwordWord3{0x31005facu, 0x31006facu, 0x3100bfacu, 0x3100cfacu, 0x31012facu}; -constexpr const char* SubwordNames[14] = { +constexpr const char* SubwordNames[16] = { "buffer_load_format_x 8_UINT +1", "buffer_load_format_x 8_UINT +2", "buffer_load_format_x 8_UINT +3", @@ -182,8 +182,10 @@ constexpr const char* SubwordNames[14] = { "buffer_load_format_x 8_SINT +3", "buffer_load_format_x 16_UINT +1", "buffer_load_format_x 16_UINT +2", + "buffer_load_format_x 16_UINT +3", "buffer_load_format_x 16_SINT +1", "buffer_load_format_x 16_SINT +2", + "buffer_load_format_x 16_SINT +3", "buffer_load_format_xy 8_8_UINT +1 x", "buffer_load_format_xy 8_8_UINT +1 y", "buffer_load_format_xy 8_8_UINT +2 x", @@ -200,11 +202,11 @@ std::vector SubwordCode() { }; for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 2u, offset, 1u); for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 3u, offset, 1u); - for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(0u, 4u, offset, 1u); - for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(0u, 5u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 4u, offset, 1u); + for (std::uint32_t offset = 1u; offset <= 3u; ++offset) emitLoad(0u, 5u, offset, 1u); for (std::uint32_t offset = 1u; offset <= 2u; ++offset) emitLoad(1u, 6u, offset, 2u); code.push_back(0xbf8c3f70u); - for (std::uint32_t index = 0u; index < 14u; ++index) { + for (std::uint32_t index = 0u; index < 16u; ++index) { code.push_back(0xe0701000u | (index * 4u)); code.push_back(0x80010003u | ((10u + index) << 8u)); } @@ -245,11 +247,11 @@ void CheckSubwordOffsets() { const auto s8 = [&](std::uint32_t offset) { return static_cast(static_cast(static_cast(u8(offset)))); }; const auto u16 = [&](std::uint32_t offset) { return (word >> (offset * 8u)) & 0xffffu; }; const auto s16 = [&](std::uint32_t offset) { return static_cast(static_cast(static_cast(u16(offset)))); }; - const std::array expected{ + const std::array expected{ u8(1u), u8(2u), u8(3u), s8(1u), s8(2u), s8(3u), - u16(1u), u16(2u), - s16(1u), s16(2u), + u16(1u), u16(2u), u16(3u), + s16(1u), s16(2u), s16(3u), u8(1u), u8(2u), u8(2u), u8(3u), }; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 0d76ccb36d..9246de6bda 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -819,6 +819,9 @@ std::shared_ptr buildSpecializedModule(const CompiledSh cursor += count; } materialized = std::move(compact); +#if ANYPS5_ENABLE_SPIRV_TOOLS + materialized = ValidateAndOptimizeSpirv(materialized, target.vulkanVersion, target.spirvVersion, target.nonConstantImageOffsets, false); +#endif auto module = std::make_shared(); std::map bindingNumbers; for (std::size_t cursor = 5; cursor < materialized.size();) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 8ce3cc9ca7..433c58aad1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -470,11 +470,8 @@ std::uint32_t genericFormattedLoad(SpirvValueEmitContext& context, const IrValue ? EmitValueIfElse(state, dword, u32, [&] { return readBuffer(context, instruction, addresses[component].guest, 32u); }, [&] { return EmitBdaReadBytes(context, instruction, addresses[component].guest, Binary(state, spv::OpShiftRightLogical, u32, width, constant(3u))); }) - : readBuffer(context, instruction, addresses[component].guest, 32u); - const auto subwordOffset = memory.gpuDescriptor - ? constant(0u) - : Binary(state, spv::OpShiftLeftLogical, u32, EmitAndConstant(state, addresses[component].guest, 3u), constant(3u)); - const auto bitOffset = Select(state, u32, packed, field(state, bitOffsets, component * 8u, 8u), subwordOffset); + : Binary(state, spv::OpShiftRightLogical, u32, readBuffer(context, instruction, addresses[component].guest, 32u), Select(state, u32, dword, constant(0u), Binary(state, spv::OpShiftLeftLogical, u32, EmitAndConstant(state, addresses[component].guest, 3u), constant(3u)))); + const auto bitOffset = Select(state, u32, packed, field(state, bitOffsets, component * 8u, 8u), constant(0u)); const auto unsignedField = EmitBitFieldUExtract(state, raw, bitOffset, bits[component]); const auto signedField = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldSExtract, TypeI32(state), signedField, Unary(state, spv::OpBitcast, TypeI32(state), raw), bitOffset, bits[component]); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 569298ea62..2b7b1eb792 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -85,7 +85,22 @@ Instruction Make(spv::Op op, std::initializer_list operands) { return result; } -bool PureInstruction(const Instruction& instruction) { +class IdSet { +public: + void Reset(std::size_t bound) { present.assign(bound, 0u); } + bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } + bool insert(std::uint32_t id) { + if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); + if (present[id] != 0u) return false; + present[id] = 1u; + return true; + } + +private: + std::vector present; +}; + +bool PureInstruction(const Instruction& instruction, const IdSet& glslExtInstSets = {}) { switch (Opcode(instruction)) { case spv::OpVectorExtractDynamic: case spv::OpVectorInsertDynamic: @@ -203,8 +218,9 @@ bool PureInstruction(const Instruction& instruction) { case spv::OpInBoundsPtrAccessChain: case spv::OpArrayLength: case spv::OpPhi: - case spv::OpExtInst: return true; + case spv::OpExtInst: + return instruction.size() >= 5u && glslExtInstSets.contains(instruction[3]); case spv::OpLoad: return instruction.size() == 4u || (instruction.size() == 5u && instruction[4] == spv::MemoryAccessMaskNone); default: @@ -557,21 +573,6 @@ private: std::size_t count = 0; }; -class IdSet { -public: - void Reset(std::size_t bound) { present.assign(bound, 0u); } - bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } - bool insert(std::uint32_t id) { - if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); - if (present[id] != 0u) return false; - present[id] = 1u; - return true; - } - -private: - std::vector present; -}; - class Specialization { public: explicit Specialization(std::span words) : arena(words.size()) { @@ -582,6 +583,7 @@ public: values.Reset(bound); resultTypes.Reset(bound); vectorTypes.Reset(bound); + glslExtInstSets.Reset(bound); removed.Reset(bound); aliases.Reset(bound); composites.Reset(bound); @@ -596,6 +598,7 @@ public: bool hasType = false; spv::HasResultAndType(op, &hasResult, &hasType); if (hasResult && hasType) resultTypes.emplace(instruction.at(2), instruction.at(1)); + if (op == spv::OpExtInstImport && instruction.size() == 6u && instruction[2] == 0x4c534c47u && instruction[3] == 0x6474732eu && instruction[4] == 0x3035342eu && instruction[5] == 0u) glslExtInstSets.insert(instruction.at(1)); if (op == spv::OpTypeBool) types.emplace(instruction.at(1), ScalarType{1u, true}); if (op == spv::OpTypeInt) types.emplace(instruction.at(1), ScalarType{instruction.at(2), false}); if (op == spv::OpTypeVector) vectorTypes.insert(instruction.at(1)); @@ -1125,7 +1128,7 @@ private: } continue; } - if (PureInstruction(instruction)) { + if (PureInstruction(instruction, glslExtInstSets)) { definitions.emplace(Result(instruction), index); continue; } @@ -1162,6 +1165,7 @@ private: IdTable values; IdTable resultTypes; IdSet vectorTypes; + IdSet glslExtInstSets; IdSet removed; IdTable aliases; IdTable> composites; From b68c03b4e40515034bcb149cf1b709f40cdf7fef Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 08:10:32 +0000 Subject: [PATCH 109/318] test(agc): check the excluded ABI by its committed state, after its front end The still-waits check in CommittedAbis timed out after 200 ms, which the other thread's front-end build alone could reach before it ever waited on the preparation lock. It now waits until that build has started, gives the thread 500 ms, and also requires the pixel shader's committed ABIs to be unchanged. The build counter is read after background registration preparation has settled, so an earlier test's preparation can't count. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/PreparedShaders.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp index b72d48eb52..bbd698465b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PreparedShaders.cpp @@ -1220,7 +1220,17 @@ void CommittedAbis() { scope.Commit(); }; resolve(); + const auto registry = AgcDriver::DriverDetail::Driver::Get().RegisteredShaders(); + Require(registry != nullptr, "committed ABI resolution published no shader registry"); + const auto pixelSnapshot = AgcDriver::DriverDetail::RegisteredHeader(*registry, &pixel.shader); + Require(pixelSnapshot != nullptr, "committed ABI resolution published no pixel snapshot"); + const auto committedAbis = [&] { + std::lock_guard lock(pixelSnapshot->prepared->mutex); + return pixelSnapshot->prepared->registeredAbis.size(); + }; + AgcDriver::DriverDetail::AwaitRegistrationPreparation(); const auto before = ShaderRecompiler::FrontEndBuilds(); + const auto abisBefore = committedAbis(); bool unblocked = false; bool excluded = false; std::uint64_t repeatedBuilds = 0; @@ -1234,7 +1244,9 @@ void CommittedAbis() { unblocked = repeated.wait_for(std::chrono::seconds(10)) == std::future_status::ready; repeatedBuilds = ShaderRecompiler::FrontEndBuilds() - before; other = std::async(std::launch::async, resolveOther); - excluded = other.wait_for(std::chrono::milliseconds(200)) == std::future_status::timeout; + const auto started = std::chrono::steady_clock::now(); + while (ShaderRecompiler::FrontEndBuilds() == before + repeatedBuilds && std::chrono::steady_clock::now() - started < std::chrono::seconds(10)) std::this_thread::yield(); + excluded = other.wait_for(std::chrono::milliseconds(500)) == std::future_status::timeout && committedAbis() == abisBefore; open.Commit(); } repeated.get(); From 103484d0be3e359f2fe577af09b4bf59ca61b7eb Mon Sep 17 00:00:00 2001 From: dantdmdarealguy Date: Wed, 21 Oct 2026 03:54:02 -0500 Subject: [PATCH 110/318] fix(tools): read the EnvironmentNumber environment reader in env_reference The Windows crash report reads APS5_CRASH_STACK_BYTES through EnvironmentNumber, which env_reference did not recognize, so the name had no row in docs/dev/ENVIRONMENT.md and neither the reference check nor the conventions check reported it. Recognize the reader as an integer reader and add the row. --- docs/dev/ENVIRONMENT.md | 3 ++- tools/env_reference.py | 5 +++-- tools/tests/test_env_reference.py | 6 +++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 7e95e1fac5..7b62e1dc64 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -1,6 +1,6 @@ # Environment variables -The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting`, `MaskFromEnvironment`, `EnvInt`, `GetEnvironmentVariableA`, `GetEnvironmentVariableW` with a wide literal, `configured`, `PipelineSwitch`, `environmentCount` and `addWideString` (the wide names the Windows entry stub reads). This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. +The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetting`, `MaskFromEnvironment`, `EnvInt`, `GetEnvironmentVariableA`, `GetEnvironmentVariableW` with a wide literal, `configured`, `PipelineSwitch`, `environmentCount`, `EnvironmentNumber` and `addWideString` (the wide names the Windows entry stub reads). This table lists every `APS5_*` and `ANYPS5_*` name it reads, its kind, the value used when it is unset and what it does, one table per library. Regenerate the names with `python3 tools/env_reference.py --table`; the Conventions check fails when a name the code reads has no row here, or a row's name is no longer read. ### core/libs @@ -532,6 +532,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| | `APS5_CRASH_INDIRECT_MEMORY` | presence | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | dumps the words the faulting registers point at in the Windows crash report | +| `APS5_CRASH_STACK_BYTES` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | 0 | bytes of the faulting thread's stack from rsp, decimal or 0x hexadecimal and at most 1048576, that the Windows crash report prints with the data at the stack words pointing outside the stack; 0 prints none | | `APS5_HANG_DUMP_SECS` | integer | `core/libs/prx/libc/src/specifics/windows/CrashReport.cpp` | | seconds after which a watchdog dumps all threads and aborts | | `APS5_HEAP_CACHE_MIB` | integer | `core/libs/prx/libc/src/GuestHeap.cpp` | 4096 | size in MiB of the cache of freed large blocks before the oldest are decommitted | | `APS5_HELPER_AFFINITY_MASK` | integer | `core/libs/prx/libc/include/CpuTopology.hpp` | | hex core mask that helper threads (deferred release, mirror refresh) are pinned to | diff --git a/tools/env_reference.py b/tools/env_reference.py index 683b1e9168..1b82ce47a9 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -10,12 +10,12 @@ ROOTS = ("core",) EXTENSIONS = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp", ".py"} SKIP = {"build", "build-linux", "build-relinker", "3rdparty", ".git"} -INTEGER_READERS = {"kibSetting", "MaskFromEnvironment", "EnvInt", "configured", "environmentCount"} +INTEGER_READERS = {"kibSetting", "MaskFromEnvironment", "EnvInt", "configured", "environmentCount", "EnvironmentNumber"} STRING_READERS = {"GetEnvironmentVariableA", "GetEnvironmentVariableW", "addWideString"} NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?_?w?getenv\(") INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64|_wtoi|_wtol|_wtoi64)\s*\(") GREP = (r'(getenv|_wgetenv|wgetenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariable[AW]' - r'|configured|PipelineSwitch|environmentCount|addWideString)' + r'|configured|PipelineSwitch|environmentCount|EnvironmentNumber|addWideString)' r'\([[:space:]]*(u|L)?"(APS5|ANYPS5)_[A-Za-z0-9_]+"') @@ -36,6 +36,7 @@ def forms(prefixes=PREFIXES): ("configured", re.compile(r'\b(?:configured|PipelineSwitch)\(\s*"(?P' + name + r')"' r'(?:\s*,\s*(?P[0-9]+)[uUlL]*)?')), ("environmentCount", re.compile(r'\benvironmentCount\(\s*"(?P' + name + r')"\s*,\s*(?P[0-9]+)')), + ("EnvironmentNumber", re.compile(r'\bEnvironmentNumber\(\s*"(?P' + name + r')"\s*\)')), ("GetEnvironmentVariableA", re.compile(r'GetEnvironmentVariableA\(\s*"(?P' + name + r')"')), ("GetEnvironmentVariableW", re.compile(r'GetEnvironmentVariableW\(\s*L"(?P' + name + r')"')), ("addWideString", re.compile(r'\baddWideString\(\s*u"(?P' + name + r')"\s*\)')), diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index c414e314c0..7ac385e9cc 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -30,6 +30,8 @@ static bool q = _wgetenv(L"APS5_WIDE_PRESENT") != nullptr; static int r = _wtoi(_wgetenv(L"APS5_WIDE_INT")); static const wchar_t* s = _wgetenv(L"ANYPS5_WIDE_CONFIG"); static const char* t = std::getenv("NOT_AN_ENV"); +static std::uint64_t u = std::min(EnvironmentNumber("APS5_CRASH_STACK_BYTES"), std::uint64_t{1} << 20); +static std::uint64_t v = MyEnvironmentNumber("APS5_NOT_A_NUMBER_READ"); """ @@ -52,7 +54,8 @@ class EnvReferenceTests(unittest.TestCase): "APS5_CRASH_INDIRECT_MEMORY", "APS5_PIPELINE_JOURNAL_MAX_MIB", "APS5_PIPELINE_STORE_ENTRIES", "APS5_PIPELINE_JOURNAL_THREADS", "APS5_PM4_RING", "ANYPS5_LOADEXEC_ARGV_SKIP", "ANYPS5_LOADEXEC_PARENT", - "ANYPS5_WIDE_CONFIG", "APS5_WIDE_PRESENT", "APS5_WIDE_INT"}) + "ANYPS5_WIDE_CONFIG", "APS5_WIDE_PRESENT", "APS5_WIDE_INT", + "APS5_CRASH_STACK_BYTES"}) self.assertEqual(found["APS5_PROFILE_DRAW"][0][1], 1) self.assertEqual(found["APS5_THREAD_NICE"][0][2:], ("integer", "-10")) self.assertEqual(found["APS5_HOST_IMPORT_MIB"][0][2:], ("integer", "6144")) @@ -65,6 +68,7 @@ class EnvReferenceTests(unittest.TestCase): self.assertEqual(found["ANYPS5_WIDE_CONFIG"][0][2:], ("string", "")) self.assertEqual(found["APS5_WIDE_PRESENT"][0][2:], ("presence", "")) self.assertEqual(found["APS5_WIDE_INT"][0][2:], ("integer", "")) + self.assertEqual(found["APS5_CRASH_STACK_BYTES"][0][2:], ("integer", "")) def test_the_default_prefixes_cover_both(self): root = self.fixture() From 2fc26b970fdc04b2b78aef256432c2fa1e39a783 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 05:18:18 -0400 Subject: [PATCH 111/318] fix(libkernel): return ESRCH for a null semaphore handle Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/Semaphore/src/Semaphore.cpp | 19 ++++++++++++++----- core/libs/tests/GuestSemaphore.cpp | 9 +++++++++ docs/dev/debt/libkernel.md | 1 + 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 725f297e67..eab1d9b52d 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -31,7 +31,10 @@ int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t at } int APS5_VABI sceKernelPollSema(KernelSema sem, int need) { - if (sem == nullptr || need <= 0) { + if (sem == nullptr) { + return SCE_KERNEL_ERROR_ESRCH; + } + if (need <= 0) { APS5_INVALID_ARG_EX; } @@ -44,7 +47,10 @@ int APS5_VABI sceKernelPollSema(KernelSema sem, int need) { } int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { - if (sem == nullptr || count < 0) { + if (sem == nullptr) { + return SCE_KERNEL_ERROR_ESRCH; + } + if (count < 0) { APS5_INVALID_ARG_EX; } @@ -60,7 +66,10 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { } int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) { - if (sem == nullptr || need <= 0) { + if (sem == nullptr) { + return SCE_KERNEL_ERROR_ESRCH; + } + if (need <= 0) { APS5_INVALID_ARG_EX; } @@ -129,7 +138,7 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads) { if (sem == nullptr) { - APS5_INVALID_ARG_EX; + return SCE_KERNEL_ERROR_ESRCH; } std::lock_guard lock(sem->mutex); @@ -148,7 +157,7 @@ int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads) { int APS5_VABI sceKernelDeleteSema(KernelSema sem) { if (sem == nullptr) { - APS5_INVALID_ARG_EX; + return SCE_KERNEL_ERROR_ESRCH; } std::unique_lock lock(sem->mutex); diff --git a/core/libs/tests/GuestSemaphore.cpp b/core/libs/tests/GuestSemaphore.cpp index 5392fa1603..0ba58d4520 100644 --- a/core/libs/tests/GuestSemaphore.cpp +++ b/core/libs/tests/GuestSemaphore.cpp @@ -15,6 +15,7 @@ int APS5_VABI sceKernelCancelSema(KernelSema sem, int count, int* threads); } static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); @@ -36,6 +37,14 @@ static void* APS5_VABI Wait(void* arg) { } int main() { + KernelUseconds nullTimeout = 1000; + int nullWaiters = -1; + Require(sceKernelWaitSema(nullptr, 1, nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelWaitSema(nullptr, 1, &nullTimeout) == SCE_KERNEL_ERROR_ESRCH && nullTimeout == 1000); + Require(sceKernelPollSema(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelSignalSema(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelCancelSema(nullptr, 0, &nullWaiters) == SCE_KERNEL_ERROR_ESRCH && nullWaiters == -1); + Require(sceKernelDeleteSema(nullptr) == SCE_KERNEL_ERROR_ESRCH); KernelSema sem = nullptr; Require(sceKernelCreateSema(&sem, "cancel", 0, 1, 3, nullptr) == SCE_OK); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6384b00c9f..d1d8a5c3ac 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -61,6 +61,7 @@ - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware - [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified. +- [sceKernelWaitSema, sceKernelPollSema, sceKernelSignalSema, sceKernelCancelSema, sceKernelDeleteSema](../../../core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp) (libkernel) - `SCE_KERNEL_ERROR_ESRCH` for a null handle follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/60c42b925c3d1e00008621b8587819999260ec31/src/core/libraries/kernel/threads/semaphore.cpp#L329-L366) (PS4), unverified on PS5. Handles are pointers, so a non-null handle that is not a live semaphore is not detected; shadPS4 returns `ESRCH` for any unallocated handle. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) From a2e07a5eb57572d36a9923e38f88ac229024e416 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:45:51 -0300 Subject: [PATCH 112/318] test(libc): check host stdout after freopen --- core/libs/tests/GuestStandardStreams.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index c37c580e6b..c5700d00b9 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -17,6 +17,7 @@ #include #ifdef _WIN32 #include +#include #else #include #endif @@ -920,6 +921,14 @@ int main(int argc, char** argv) { CheckStandardStreamDeleteSharing(); const std::string standardFilename = "anyps5-standard-reopen-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); Require(freopen_nid_postfix(standardFilename.c_str(), "w", __stdoutp_nid_postfix) == __stdoutp_nid_postfix); + const HANDLE hostOutput = ::GetStdHandle(STD_OUTPUT_HANDLE); + Require(hostOutput != nullptr && hostOutput != INVALID_HANDLE_VALUE); + Require(hostOutput != reinterpret_cast(::_get_osfhandle(::_fileno(stdout)))); + DWORD handleFlags = 0; + Require(::GetHandleInformation(hostOutput, &handleFlags) != 0); + constexpr char hostMessage[] = "host stdout preserved\n"; + DWORD hostWritten = 0; + Require(::WriteFile(hostOutput, hostMessage, sizeof(hostMessage) - 1, &hostWritten, nullptr) != 0 && hostWritten == sizeof(hostMessage) - 1); Require(printf_nid_postfix("standard output") == 15 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); { std::ifstream input(standardFilename, std::ios::binary); std::string contents; std::getline(input, contents); Require(contents == "standard output"); } From 1860f903040b49cf0e227e500faaa3d28aea72b1 Mon Sep 17 00:00:00 2001 From: hypermist Date: Wed, 21 Oct 2026 22:47:12 +1300 Subject: [PATCH 113/318] feat(libc): add std::system_category and store error_code values at offset 4 The console error_code and error_condition keep the value in the second dword: the shared error_category::equivalent reads it at +4, the default_error_condition overrides return it shifted into the high half, and title code built against the SDK headers stores 0 at +0 and reads value() at +4. The generic category returned and compared the value at +0. system_category returns a static _System_error_category named "system" that shares the generic message table and maps a value with a message to the generic category in default_error_condition. The equivalent overloads are shared by both categories and go through the object's own default_error_condition, as the console's base implementation does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- .../libc/src/exception/StandardExceptions.cpp | 56 +++++++++++++------ core/libs/tests/GuestAmprCppError.cpp | 8 +-- core/libs/tests/GuestAmprThreads.cpp | 1 + core/libs/tests/GuestGenericCategory.cpp | 44 ++++++++++++++- 4 files changed, 84 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index 31feeb3d8e..bf6b539f82 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -129,9 +129,10 @@ ExceptionObject* Assign(ExceptionObject* object, const ExceptionObject* source) } struct CategoryObject { const void* vtable; }; -struct ErrorCode { int value; const CategoryObject* category; }; +struct ErrorCode { int reserved; int value; const CategoryObject* category; }; struct SystemErrorObject { ExceptionObject base; ErrorCode code; }; -static_assert(sizeof(ErrorCode) == 16 && sizeof(SystemErrorObject) == 32 && offsetof(SystemErrorObject, code) == 16); +static_assert(sizeof(ErrorCode) == 16 && offsetof(ErrorCode, value) == 4 && offsetof(ErrorCode, category) == 8); +static_assert(sizeof(SystemErrorObject) == 32 && offsetof(SystemErrorObject, code) == 16); struct CppError { int value; const char* message; }; static const char* GenericErrorMessage(int value) { @@ -216,13 +217,15 @@ static const char* GenericErrorMessage(int value) { const TypeRecord ErrorCategoryType {ClassTypeVtable + 2, "St14error_category", nullptr}; const TypeRecord GenericCategoryType {SingleTypeVtable + 2, "St23_Generic_error_category", &ErrorCategoryType}; -static void APS5_VABI GenericCategoryDestroy_nid_no_patch(CategoryObject*) {} -static void APS5_VABI GenericCategoryDelete_nid_no_patch(CategoryObject*) { - throw std::invalid_argument("std::_Generic_error_category: the generic category object is not heap allocated"); +const TypeRecord SystemCategoryType {SingleTypeVtable + 2, "St22_System_error_category", &ErrorCategoryType}; +static void APS5_VABI CategoryDestroy_nid_no_patch(CategoryObject*) {} +static void APS5_VABI CategoryDelete_nid_no_patch(CategoryObject*) { + throw std::invalid_argument("std::error_category: the generic and system category objects are not heap allocated"); } static const char* APS5_VABI GenericCategoryName_nid_no_patch(const CategoryObject*) { return "generic"; } -static GuestLocale::String* APS5_VABI GenericCategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) { - if (!result) throw std::invalid_argument("std::_Generic_error_category::message: null result"); +static const char* APS5_VABI SystemCategoryName_nid_no_patch(const CategoryObject*) { return "system"; } +static GuestLocale::String* APS5_VABI CategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) { + if (!result) throw std::invalid_argument("std::error_category::message: null result"); const char* message = GenericErrorMessage(value); if (!message) message = "unknown error"; const auto length = std::strlen(message); @@ -235,22 +238,36 @@ static GuestLocale::String* APS5_VABI GenericCategoryMessage_nid_no_patch(GuestL result->capacity = capacity; return result; } -static ErrorCode APS5_VABI GenericCategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {value, self}; } -static bool APS5_VABI GenericCategoryEquivalentCondition_nid_no_patch(const CategoryObject* self, int value, const ErrorCode* condition) { - return condition->category == self && condition->value == value; +static ErrorCode APS5_VABI CategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {0, value, self}; } +static bool APS5_VABI CategoryEquivalentCondition_nid_no_patch(const CategoryObject* self, int value, const ErrorCode* condition) { + using Condition = ErrorCode (APS5_VABI *)(const CategoryObject*, int); + const auto mapped = reinterpret_cast(static_cast(self->vtable)[4])(self, value); + return condition->category == mapped.category && condition->value == mapped.value; } -static bool APS5_VABI GenericCategoryEquivalentCode_nid_no_patch(const CategoryObject* self, const ErrorCode* code, int value) { +static bool APS5_VABI CategoryEquivalentCode_nid_no_patch(const CategoryObject* self, const ErrorCode* code, int value) { return code->category == self && code->value == value; } const void* const GenericCategoryVtable[] { nullptr, &GenericCategoryType, - reinterpret_cast(GenericCategoryDestroy_nid_no_patch), reinterpret_cast(GenericCategoryDelete_nid_no_patch), - reinterpret_cast(GenericCategoryName_nid_no_patch), reinterpret_cast(GenericCategoryMessage_nid_no_patch), - reinterpret_cast(GenericCategoryCondition_nid_no_patch), - reinterpret_cast(GenericCategoryEquivalentCondition_nid_no_patch), - reinterpret_cast(GenericCategoryEquivalentCode_nid_no_patch) + reinterpret_cast(CategoryDestroy_nid_no_patch), reinterpret_cast(CategoryDelete_nid_no_patch), + reinterpret_cast(GenericCategoryName_nid_no_patch), reinterpret_cast(CategoryMessage_nid_no_patch), + reinterpret_cast(CategoryCondition_nid_no_patch), + reinterpret_cast(CategoryEquivalentCondition_nid_no_patch), + reinterpret_cast(CategoryEquivalentCode_nid_no_patch) }; const CategoryObject GenericCategory {GenericCategoryVtable + 2}; +static ErrorCode APS5_VABI SystemCategoryCondition_nid_no_patch(const CategoryObject* self, int value) { + return {0, value, GenericErrorMessage(value) ? &GenericCategory : self}; +} +const void* const SystemCategoryVtable[] { + nullptr, &SystemCategoryType, + reinterpret_cast(CategoryDestroy_nid_no_patch), reinterpret_cast(CategoryDelete_nid_no_patch), + reinterpret_cast(SystemCategoryName_nid_no_patch), reinterpret_cast(CategoryMessage_nid_no_patch), + reinterpret_cast(SystemCategoryCondition_nid_no_patch), + reinterpret_cast(CategoryEquivalentCondition_nid_no_patch), + reinterpret_cast(CategoryEquivalentCode_nid_no_patch) +}; +const CategoryObject SystemCategory {SystemCategoryVtable + 2}; } extern "C" { @@ -260,9 +277,12 @@ const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value) { const LibcException::CategoryObject* APS5_VABI _ZSt16generic_categoryv_nid_postfix() { return &LibcException::GenericCategory; } +const LibcException::CategoryObject* APS5_VABI _ZSt15system_categoryv_nid_postfix() { + return &LibcException::SystemCategory; +} GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( GuestLocale::String* result, const LibcException::CategoryObject* self, int value) { - return LibcException::GenericCategoryMessage_nid_no_patch(result, self, value); + return LibcException::CategoryMessage_nid_no_patch(result, self, value); } LibcException::TypeRecord _ZTISt8ios_base_nid_postfix {LibcException::ClassTypeVtable + 2, "St8ios_base", nullptr}; LibcException::TypeRecord _ZTISt9exception_nid_postfix {LibcException::ClassTypeVtable + 2, "St9exception", nullptr}; @@ -509,7 +529,7 @@ const char* APS5_VABI _ZNKSt8ios_base7failure4whatEv_nid_postfix(const LibcExcep std::snprintf(message, sizeof(message), "%s: %s", error.message, LibcException::GenericErrorMessage(error.value)); auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(LibcException::SystemErrorObject))); LibcException::Construct(&object->base, _ZTVSt12system_error_nid_postfix, message); - object->code = {error.value, &LibcException::GenericCategory}; + object->code = {0, error.value, &LibcException::GenericCategory}; __cxa_throw_nid_postfix(object, reinterpret_cast(&_ZTISt12system_error_nid_postfix), reinterpret_cast(LibcException::DestroyGuestMessage_nid_no_patch)); } diff --git a/core/libs/tests/GuestAmprCppError.cpp b/core/libs/tests/GuestAmprCppError.cpp index 8e2407c199..2cd8eceea5 100644 --- a/core/libs/tests/GuestAmprCppError.cpp +++ b/core/libs/tests/GuestAmprCppError.cpp @@ -12,7 +12,7 @@ struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; struct Category { const void* const* vtable; }; -struct ErrorCode { int value; const Category* category; }; +struct ErrorCode { int reserved; int value; const Category* category; }; struct SystemError { const void* const* vtable; const char* message; ErrorCode code; }; static_assert(sizeof(SystemError) == 32 && offsetof(SystemError, code) == 16); @@ -80,8 +80,8 @@ static void CheckCategory(const SystemError& error) { Require(std::strcmp(Slot(category->vtable, 2)(category), "generic") == 0, 23); const ErrorCode condition = Slot(category->vtable, 4)(category, error.code.value); Require(condition.value == error.code.value && condition.category == category, 24); - const ErrorCode other {error.code.value + 1, category}; - const ErrorCode foreign {error.code.value, nullptr}; + const ErrorCode other {0, error.code.value + 1, category}; + const ErrorCode foreign {0, error.code.value, nullptr}; auto equivalentCondition = Slot(category->vtable, 5); Require(equivalentCondition(category, error.code.value, &condition), 25); Require(!equivalentCondition(category, error.code.value, &other), 26); @@ -111,7 +111,7 @@ static void CheckCopies(const SystemError& error) { Require(copy.code.value == error.code.value && copy.code.category == error.code.category, 41); SystemError assigned {}; _ZNSt12system_errorC1ERKS__nid_postfix(&assigned, &error); - assigned.code = {0, nullptr}; + assigned.code = {0, 0, nullptr}; Require(_ZNSt12system_erroraSERKS__nid_postfix(&assigned, &error) == &assigned, 42); Require(assigned.code.value == error.code.value && assigned.code.category == error.code.category, 43); _ZNSt12system_errorD1Ev_nid_postfix(&assigned); diff --git a/core/libs/tests/GuestAmprThreads.cpp b/core/libs/tests/GuestAmprThreads.cpp index 403c445304..5467abcebd 100644 --- a/core/libs/tests/GuestAmprThreads.cpp +++ b/core/libs/tests/GuestAmprThreads.cpp @@ -38,6 +38,7 @@ struct GuestTypeInfo { struct GuestSystemError { const void* vtable; const char* message; + int reserved; int code; const void* category; }; diff --git a/core/libs/tests/GuestGenericCategory.cpp b/core/libs/tests/GuestGenericCategory.cpp index b4f3f98687..bc279a1a56 100644 --- a/core/libs/tests/GuestGenericCategory.cpp +++ b/core/libs/tests/GuestGenericCategory.cpp @@ -9,14 +9,19 @@ #include struct Category { void* const* vtable; }; -struct ErrorCode { int value; const Category* category; }; +struct ErrorCode { int reserved; int value; const Category* category; }; using Message = GuestLocale::String* (APS5_VABI *)(GuestLocale::String*, const Category*, int); using Name = const char* (APS5_VABI *)(const Category*); using Condition = ErrorCode (APS5_VABI *)(const Category*, int); +using EquivalentCondition = bool (APS5_VABI *)(const Category*, int, const ErrorCode*); +using EquivalentCode = bool (APS5_VABI *)(const Category*, const ErrorCode*, int); +using Delete = void (APS5_VABI *)(const Category*); +struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; extern "C" { const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value); const Category* APS5_VABI _ZSt16generic_categoryv_nid_postfix(); +const Category* APS5_VABI _ZSt15system_categoryv_nid_postfix(); GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( GuestLocale::String* result, const Category* self, int value); int* APS5_VABI __error_nid_postfix(); @@ -48,6 +53,36 @@ static void CheckMessage(Message message, const Category* category, int value, c if (length > 15) _ZdlPv_nid_postfix(guarded.result.pointer); } +static void CheckSystemCategory(const Category* generic) { + const Category* system = _ZSt15system_categoryv_nid_postfix(); + REQUIRE(system && system != generic && system == _ZSt15system_categoryv_nid_postfix()); + const auto* type = static_cast(system->vtable[-1]); + REQUIRE(std::strcmp(type->name, "St22_System_error_category") == 0); + REQUIRE(type->base && std::strcmp(type->base->name, "St14error_category") == 0); + REQUIRE(std::strcmp(reinterpret_cast(system->vtable[2])(system), "system") == 0); + const auto condition = reinterpret_cast(system->vtable[4]); + const ErrorCode known = condition(system, 2); + std::uint64_t head = 0; + std::memcpy(&head, &known, sizeof(head)); + REQUIRE(head == std::uint64_t{2} << 32 && known.value == 2 && known.category == generic); + const ErrorCode unknown = condition(system, 44); + REQUIRE(unknown.reserved == 0 && unknown.value == 44 && unknown.category == system); + const auto equivalentCondition = reinterpret_cast(system->vtable[5]); + REQUIRE(equivalentCondition(system, 2, &known)); + const ErrorCode sameCategory {0, 2, system}; + REQUIRE(!equivalentCondition(system, 2, &sameCategory)); + REQUIRE(equivalentCondition(system, 44, &unknown)); + const auto equivalentCode = reinterpret_cast(system->vtable[6]); + REQUIRE(equivalentCode(system, &sameCategory, 2) && !equivalentCode(system, &known, 2)); + const auto message = reinterpret_cast(system->vtable[3]); + CheckMessage(message, system, 2, "no such file or directory"); + CheckMessage(message, system, 44, "unknown error"); + bool rejected = false; + try { reinterpret_cast(system->vtable[1])(system); } + catch (const std::invalid_argument&) { rejected = true; } + REQUIRE(rejected); +} + int main() { const std::array api {}; ApplicationHeapRegister_nid_no_patch(api.data()); @@ -56,7 +91,10 @@ int main() { REQUIRE(std::strcmp(reinterpret_cast(category->vtable[2])(category), "generic") == 0); const auto message = reinterpret_cast(category->vtable[3]); const auto condition = reinterpret_cast(category->vtable[4])(category, 35); - REQUIRE(condition.value == 35 && condition.category == category); + REQUIRE(condition.reserved == 0 && condition.value == 35 && condition.category == category); + const auto equivalent = reinterpret_cast(category->vtable[5]); + REQUIRE(equivalent(category, 35, &condition)); + CheckSystemCategory(category); struct Expected { int value; const char* text; }; const Expected expected[] { @@ -95,5 +133,5 @@ int main() { try { message(nullptr, category, 22); } catch (const std::invalid_argument&) { rejected = true; } REQUIRE(rejected); - std::puts("guest generic category, FreeBSD error map and message ownership: PASS"); + std::puts("guest generic and system categories, FreeBSD error map and message ownership: PASS"); } From 3b426ed9a41095304ed44535fa7015ed09adaa7c Mon Sep 17 00:00:00 2001 From: priyanshrv-netizen <275132450+priyanshrv-netizen@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:47:06 +0530 Subject: [PATCH 114/318] test(libc): run CheckVacopy in guest_stdio_variadic main never called it, so the _Vacopy checks were dropped from the Release build and never ran. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/tests/GuestStdioVariadic.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/core/libs/tests/GuestStdioVariadic.cpp b/core/libs/tests/GuestStdioVariadic.cpp index 7bc4ba7cdb..99e28777ff 100644 --- a/core/libs/tests/GuestStdioVariadic.cpp +++ b/core/libs/tests/GuestStdioVariadic.cpp @@ -141,5 +141,6 @@ int main() { char16_t letters[4]{}; Require(ScanWideSized(u"abcdef", u"%3ls", letters, 4u) == 1 && std::u16string(letters) == u"abc", "vswscanf_s %ls with a size"); Require(ScanWideSized(u"abcdef", u"%ls", letters, 4u) == 0 && letters[0] == 0, "vswscanf_s rejects a too small buffer"); + CheckVacopy(); return 0; } From 683fc312fa7f54bd36de3c77229f47026d92d362 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 13:18:56 +0300 Subject: [PATCH 115/318] feat(libSceNpCommon): compare, order and validate online IDs and NpIds --- core/libs/prx/libSceNpCommon/Export.cpp | 108 ++++++++++++++++++ .../prx/libSceNpCommon/PendingExports.cpp | 20 ---- docs/dev/debt/libSceNpCommon.md | 6 +- 3 files changed, 109 insertions(+), 25 deletions(-) create mode 100644 core/libs/prx/libSceNpCommon/Export.cpp diff --git a/core/libs/prx/libSceNpCommon/Export.cpp b/core/libs/prx/libSceNpCommon/Export.cpp new file mode 100644 index 0000000000..97a06c816c --- /dev/null +++ b/core/libs/prx/libSceNpCommon/Export.cpp @@ -0,0 +1,108 @@ +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NP_ERROR_INVALID_ARGUMENT = static_cast(0x80550003); +constexpr int NP_ERROR_INVALID_PLATFORM_TYPE = static_cast(0x80550004); +constexpr int NP_UTIL_ERROR_NOT_MATCH = static_cast(0x80550609); +constexpr int NP_LW_COND_ERROR_TIMEDOUT = static_cast(0x8055800B); +constexpr int NP_LW_MUTEX_ERROR_BUSY = static_cast(0x8055800F); +constexpr int KERNEL_ERROR_EBUSY = static_cast(0x80020010); +constexpr int KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +constexpr std::size_t ONLINE_ID_MAX_LENGTH = 16; +constexpr int MUTEX_TYPE_RECURSIVE = 2; +constexpr KernelClockid CLOCK_MONOTONIC_ID = 4; + +struct NpIdFull { + NpOnlineId handle; + std::uint8_t opt[8]; + std::uint8_t reserved[8]; +}; +static_assert(sizeof(NpIdFull) == 36); + +struct SystemSwVersion { + std::uint64_t size; + char versionString[0x1C]; + std::uint32_t version; +}; +static_assert(sizeof(SystemSwVersion) == 0x28); + +int compareBytes(const std::uint8_t* lhs, const std::uint8_t* rhs, std::size_t count) { + for (std::size_t i = 0; i < count; ++i) { + if (lhs[i] != rhs[i]) return lhs[i] < rhs[i] ? -1 : 1; + } + return 0; +} + +} + +extern "C" int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +extern "C" int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +extern "C" int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); +extern "C" int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +extern "C" int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadCondattrInit(PthreadCondattr* attr); +extern "C" int APS5_VABI scePthreadCondattrDestroy(PthreadCondattr* attr); +extern "C" int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); +extern "C" int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); +extern "C" int APS5_VABI scePthreadCondSignal(PthreadCond* cond); +extern "C" int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +extern "C" int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec); +extern "C" int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +extern "C" int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); +extern "C" int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SystemSwVersion* version); + +extern "C" { + +int APS5_VABI sceNpCmpOnlineId(const NpOnlineId* onlineId1, const NpOnlineId* onlineId2) { + if (!onlineId1 || !onlineId2) return NP_ERROR_INVALID_ARGUMENT; + if (std::strncmp(onlineId1->data, onlineId2->data, ONLINE_ID_MAX_LENGTH) != 0) return NP_UTIL_ERROR_NOT_MATCH; + return 0; +} + +int APS5_VABI sceNpCmpNpIdInOrder(const NpIdFull* npId1, const NpIdFull* npId2, std::int32_t* order) { + if (!npId1 || !npId2 || !order) return NP_ERROR_INVALID_ARGUMENT; + int compare = std::strncmp(npId1->handle.data, npId2->handle.data, ONLINE_ID_MAX_LENGTH); + if (compare == 0) compare = compareBytes(npId1->opt, npId2->opt, sizeof(npId1->opt)); + if (compare == 0) compare = compareBytes(npId1->reserved, npId2->reserved, sizeof(npId1->reserved)); + *order = compare < 0 ? -1 : compare > 0 ? 1 : 0; + return 0; +} + +int APS5_VABI sceNpGetPlatformType(const NpIdFull* npId) { + constexpr std::uint32_t TAG_PS3 = 0x00337370; + constexpr std::uint32_t TAG_VITA = 0x32707370; + constexpr std::uint32_t TAG_PS4 = 0x00347370; + if (!npId) return NP_ERROR_INVALID_ARGUMENT; + if (npId->opt[4] == 0) return 0; + std::uint32_t tag = 0; + std::memcpy(&tag, npId->opt + 4, sizeof(tag)); + switch (tag) { + case TAG_PS3: return 1; + case TAG_VITA: return 2; + case TAG_PS4: return 3; + default: return NP_ERROR_INVALID_PLATFORM_TYPE; + } +} + +int APS5_VABI sceNpIntIsValidOnlineId(const NpOnlineId* onlineId) { + if (!onlineId || onlineId->data[ONLINE_ID_MAX_LENGTH] != 0) return 0; + const std::size_t length = strnlen(onlineId->data, ONLINE_ID_MAX_LENGTH + 1); + if (length < 3 || length > ONLINE_ID_MAX_LENGTH) return 0; + if (!std::isalnum(static_cast(onlineId->data[0]))) return 0; + for (std::size_t i = 1; i < length; ++i) { + const unsigned char character = static_cast(onlineId->data[i]); + if (!std::isalnum(character) && character != '_' && character != '-') return 0; + } + return 1; +} + +} diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index c9d6e53ffd..3269532314 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -115,16 +115,6 @@ int APS5_VABI sceNpCloseSema(void) { return 0; } -int APS5_VABI sceNpCmpNpIdInOrder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCmpOnlineId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCondDestroy(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -210,11 +200,6 @@ int APS5_VABI sceNpGetNavSdkVersion(void) { return 0; } -int APS5_VABI sceNpGetPlatformType(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGetProcessId(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -310,11 +295,6 @@ int APS5_VABI sceNpIntIsOnlineIdString(void) { return 0; } -int APS5_VABI sceNpIntIsValidOnlineId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpIntSetPlatformType(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 2a8900021d..521ae4d85e 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,7 +2,7 @@ ### Unknown function info -The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. - [sceNpAllocateKernelMemoryNoAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MWPOkqzYss0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpAllocateKernelMemoryWithAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `gMlY6eewr-c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -26,8 +26,6 @@ The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpClearEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9pLoHoPMxeg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCmpNpIdInOrder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TcwEFnakiSc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCmpOnlineId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dj+O5aD2a0Q`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1a+iY5YUJcI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `q2tsVO3lM4A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uMJFOA62mVU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -45,7 +43,6 @@ The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpDeleteSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPrF2nGPBXQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpFreeKernelMemory](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vjwlDmsGtME`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `sXVQUIGmk2U`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetProcessId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Z3mnqcGmf8E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pJlGhXEt5CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Pglk7zFj0DI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -65,7 +62,6 @@ The 150 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `HoPC33siDD4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `G6qytFoBJ-w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntIsOnlineIdString](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fY4XQoA20i8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIntIsValidOnlineId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hkeX9iuCwlI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X6emt+LbSEI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntToHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TWPY1x1Atys`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIpc2ClientInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kgDwlmy78k0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. From 6fdfa33e030c6c994c8d4fdd5a9c57f9670efefb Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 13:18:56 +0300 Subject: [PATCH 116/318] feat(libSceNpCommon): wrap the libkernel mutex calls --- core/libs/prx/libSceNpCommon/Export.cpp | 47 +++++++++++++++++ .../prx/libSceNpCommon/PendingExports.cpp | 50 ------------------- docs/dev/debt/libSceNpCommon.md | 10 ---- 3 files changed, 47 insertions(+), 60 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/Export.cpp b/core/libs/prx/libSceNpCommon/Export.cpp index 97a06c816c..5eadd6d1a6 100644 --- a/core/libs/prx/libSceNpCommon/Export.cpp +++ b/core/libs/prx/libSceNpCommon/Export.cpp @@ -105,4 +105,51 @@ int APS5_VABI sceNpIntIsValidOnlineId(const NpOnlineId* onlineId) { return 1; } +int APS5_VABI sceNpMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags) { + PthreadMutexattr attr = nullptr; + int result = scePthreadMutexattrInit(&attr); + if (result != 0) return result; + if (flags & 1) result = scePthreadMutexattrSettype(&attr, MUTEX_TYPE_RECURSIVE); + if (result == 0) result = scePthreadMutexInit(mutex, &attr, name); + scePthreadMutexattrDestroy(&attr); + return result; +} + +int APS5_VABI sceNpMutexDestroy(PthreadMutex* mutex) { + return scePthreadMutexDestroy(mutex); +} + +int APS5_VABI sceNpMutexLock(PthreadMutex* mutex) { + return scePthreadMutexLock(mutex); +} + +int APS5_VABI sceNpMutexTryLock(PthreadMutex* mutex) { + const int result = scePthreadMutexTrylock(mutex); + return result == KERNEL_ERROR_EBUSY ? NP_LW_MUTEX_ERROR_BUSY : result; +} + +int APS5_VABI sceNpMutexUnlock(PthreadMutex* mutex) { + return scePthreadMutexUnlock(mutex); +} + +int APS5_VABI sceNpLwMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags) { + return sceNpMutexInit(mutex, name, flags); +} + +int APS5_VABI sceNpLwMutexDestroy(PthreadMutex* mutex) { + return sceNpMutexDestroy(mutex); +} + +int APS5_VABI sceNpLwMutexLock(PthreadMutex* mutex) { + return sceNpMutexLock(mutex); +} + +int APS5_VABI sceNpLwMutexTryLock(PthreadMutex* mutex) { + return sceNpMutexTryLock(mutex); +} + +int APS5_VABI sceNpLwMutexUnlock(PthreadMutex* mutex) { + return sceNpMutexUnlock(mutex); +} + } diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index 3269532314..3956386947 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -380,31 +380,6 @@ int APS5_VABI sceNpLwCondWait(void) { return 0; } -int APS5_VABI sceNpLwMutexDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexTryLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwMutexUnlock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpMemoryHeapDestroy(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -425,31 +400,6 @@ int APS5_VABI sceNpMemoryHeapInit(void) { return 0; } -int APS5_VABI sceNpMutexDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexTryLock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMutexUnlock(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpOpenEventFlag(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 521ae4d85e..7905ee9572 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -79,20 +79,10 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpLwCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FJ4DCt8VzVE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpLwCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Bwi+EP8VQ+g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpLwCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ExeLuE3EQCQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4zxevggtYrQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1CiXI-MyEKs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `18j+qk6dRwk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexTryLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hp0kVgu5Fxw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwMutexUnlock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CQG2oyx1-nM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dfXSH2Tsjkw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FaMNvjMA6to`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xHAiSVEEjSI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpMemoryHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kZizwrFvWZY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lQ11BpMM4LU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uEwag-0YZPc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `r9Bet+s6fKc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexTryLock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DuslmoqQ+nk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMutexUnlock](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `oZyb9ktuCpA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpOpenEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5DkyduAF2rs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpOpenSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-blITIdtUd0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpPanic](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZoXUrTiwKNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. From affc646ae7025c4488e12af2eb660bf46b41549d Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 13:18:57 +0300 Subject: [PATCH 117/318] feat(libSceNpCommon): wrap the condition, join, clock and version calls --- core/libs/prx/libSceNpCommon/Export.cpp | 42 +++++++++++++++++++ .../prx/libSceNpCommon/PendingExports.cpp | 35 ---------------- docs/dev/debt/libSceNpCommon.md | 7 ---- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/Export.cpp b/core/libs/prx/libSceNpCommon/Export.cpp index 5eadd6d1a6..f10b45437e 100644 --- a/core/libs/prx/libSceNpCommon/Export.cpp +++ b/core/libs/prx/libSceNpCommon/Export.cpp @@ -152,4 +152,46 @@ int APS5_VABI sceNpLwMutexUnlock(PthreadMutex* mutex) { return sceNpMutexUnlock(mutex); } +int APS5_VABI sceNpCondInit(PthreadCond* cond, const char* name, std::uint64_t) { + PthreadCondattr attr = nullptr; + int result = scePthreadCondattrInit(&attr); + if (result != 0) return result; + result = scePthreadCondInit(cond, &attr, name); + scePthreadCondattrDestroy(&attr); + return result; +} + +int APS5_VABI sceNpCondDestroy(PthreadCond* cond) { + return scePthreadCondDestroy(cond); +} + +int APS5_VABI sceNpCondSignal(PthreadCond* cond) { + return scePthreadCondSignal(cond); +} + +int APS5_VABI sceNpCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, std::uint32_t usec) { + if (usec == 0) return scePthreadCondWait(cond, mutex); + const int result = scePthreadCondTimedwait(cond, mutex, usec); + return result == KERNEL_ERROR_ETIMEDOUT ? NP_LW_COND_ERROR_TIMEDOUT : result; +} + +int APS5_VABI sceNpJoinThread(Pthread thread, void** retval) { + return scePthreadJoin(thread, retval); +} + +int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec) { + KernelTimespec now{}; + const int result = sceKernelClockGettime(CLOCK_MONOTONIC_ID, &now); + if (result == 0) *usec = now.tv_sec * 1000000 + now.tv_nsec / 1000; + return result; +} + +void APS5_VABI sceNpGetSdkVersion(char* version) { + SystemSwVersion swVersion{}; + swVersion.size = sizeof(swVersion); + sceKernelGetSystemSwVersion_nid_postfix(&swVersion); + const std::uint32_t hex = swVersion.version; + std::snprintf(version, 8, "%u.%02u", ((hex >> 24) & 0xF) + (hex >> 28) * 10, ((hex >> 16) & 0xF) + ((hex >> 20) & 0xF) * 10); +} + } diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index 3956386947..4326061d26 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -115,21 +115,6 @@ int APS5_VABI sceNpCloseSema(void) { return 0; } -int APS5_VABI sceNpCondDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCondInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCondSignal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCondSignalAll(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -140,11 +125,6 @@ int APS5_VABI sceNpCondSignalTo(void) { return 0; } -int APS5_VABI sceNpCondTimedwait(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCondWait(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -210,21 +190,11 @@ int APS5_VABI sceNpGetRandom(void) { return 0; } -int APS5_VABI sceNpGetSdkVersion(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGetSdkVersionUInt(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpGetSystemClockUsec(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGlobalHeapGetAllocator(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -315,11 +285,6 @@ int APS5_VABI sceNpIpc2ClientTerm(void) { return 0; } -int APS5_VABI sceNpJoinThread(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpJsonParse(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 7905ee9572..0429e246b8 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -26,12 +26,8 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpClearEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9pLoHoPMxeg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1a+iY5YUJcI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `q2tsVO3lM4A`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uMJFOA62mVU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bsjWg59A7aE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bAHIOyNnx5Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondTimedwait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ss2xO9IJxKQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fZShld2PQ7w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCreateEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `6jFWpAfqAcc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCreateSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LHZtCT2W1Pw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -45,9 +41,7 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetProcessId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Z3mnqcGmf8E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pJlGhXEt5CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Pglk7zFj0DI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetSdkVersionUInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ljqnF0hmLjo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetSystemClockUsec](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `PVVsRmMkO1g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGlobalHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-gN6uE+zWng`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGlobalHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VUHUasztbUY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGlobalHeapGetAllocatorExPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `P4YpPziLBd4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -66,7 +60,6 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpIntToHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TWPY1x1Atys`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIpc2ClientInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kgDwlmy78k0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIpc2ClientTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CI2p6Viee9w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpJoinThread](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EjMsfO3GCIA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vJGDnNh4I0g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RgfCYkjW7As`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseBufInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SnAdybtBK3o`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. From c6fa81f8611db272fa9110bc5ee7cb473dc276a0 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 13:18:57 +0300 Subject: [PATCH 118/318] test(libSceNpCommon): cover the ID utilities, sync wrappers and clock --- core/libs/CMakeLists.txt | 13 +++ core/libs/tests/GuestNpCommon.cpp | 172 ++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 core/libs/tests/GuestNpCommon.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 2854a5fa08..a1e0d5218d 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -380,6 +380,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_np_common_tests tests/GuestNpCommon.cpp) + target_include_directories(guest_np_common_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_common_tests PRIVATE libSceNpCommon libkernel libc) + configure_windows_unwind(guest_np_common_tests) + foreach(testCase IN ITEMS ids sync clock) + add_test(NAME guest_np_common_${testCase} COMMAND guest_np_common_tests ${testCase}) + set_tests_properties(guest_np_common_${testCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_common_${testCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) diff --git a/core/libs/tests/GuestNpCommon.cpp b/core/libs/tests/GuestNpCommon.cpp new file mode 100644 index 0000000000..d605767148 --- /dev/null +++ b/core/libs/tests/GuestNpCommon.cpp @@ -0,0 +1,172 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include + +struct NpIdFull { + NpOnlineId handle; + std::uint8_t opt[8]; + std::uint8_t reserved[8]; +}; + +extern "C" { +int APS5_VABI sceNpCmpOnlineId(const NpOnlineId* onlineId1, const NpOnlineId* onlineId2); +int APS5_VABI sceNpCmpNpIdInOrder(const NpIdFull* npId1, const NpIdFull* npId2, std::int32_t* order); +int APS5_VABI sceNpGetPlatformType(const NpIdFull* npId); +int APS5_VABI sceNpIntIsValidOnlineId(const NpOnlineId* onlineId); +int APS5_VABI sceNpMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags); +int APS5_VABI sceNpMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceNpMutexLock(PthreadMutex* mutex); +int APS5_VABI sceNpMutexTryLock(PthreadMutex* mutex); +int APS5_VABI sceNpMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceNpLwMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags); +int APS5_VABI sceNpLwMutexTryLock(PthreadMutex* mutex); +int APS5_VABI sceNpLwMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceNpLwMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceNpCondInit(PthreadCond* cond, const char* name, std::uint64_t flags); +int APS5_VABI sceNpCondDestroy(PthreadCond* cond); +int APS5_VABI sceNpCondSignal(PthreadCond* cond); +int APS5_VABI sceNpCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, std::uint32_t usec); +int APS5_VABI sceNpJoinThread(Pthread thread, void** retval); +int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec); +void APS5_VABI sceNpGetSdkVersion(char* version); +int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +} + +static constexpr int INVALID_ARGUMENT = static_cast(0x80550003); +static constexpr int INVALID_PLATFORM_TYPE = static_cast(0x80550004); +static constexpr int NOT_MATCH = static_cast(0x80550609); +static constexpr int COND_TIMEDOUT = static_cast(0x8055800B); +static constexpr int MUTEX_BUSY = static_cast(0x8055800F); + +static bool expect(bool condition, const char* message) { + if (!condition) std::fprintf(stderr, "%s\n", message); + return condition; +} + +static NpOnlineId onlineId(const char* text) { + NpOnlineId id{}; + std::strncpy(id.data, text, sizeof(id.data)); + return id; +} + +static NpIdFull npIdWithTag(const char* handle, std::uint32_t tag) { + NpIdFull id{}; + id.handle = onlineId(handle); + std::memcpy(id.opt + 4, &tag, sizeof(tag)); + return id; +} + +static int testIds() { + const NpOnlineId kestrel = onlineId("kestrel_77"); + const NpOnlineId kestrelCopy = onlineId("kestrel_77"); + const NpOnlineId heron = onlineId("heron-4"); + bool ok = expect(sceNpCmpOnlineId(nullptr, &kestrel) == INVALID_ARGUMENT, "a null online ID was compared"); + ok &= expect(sceNpCmpOnlineId(&kestrel, &kestrelCopy) == 0, "equal online IDs did not match"); + ok &= expect(sceNpCmpOnlineId(&kestrel, &heron) == NOT_MATCH, "different online IDs matched"); + + NpIdFull low = npIdWithTag("kestrel_77", 0x00347370); + NpIdFull high = low; + high.reserved[7] = 1; + std::int32_t order = 5; + ok &= expect(sceNpCmpNpIdInOrder(&low, &high, nullptr) == INVALID_ARGUMENT, "a null order was accepted"); + ok &= expect(sceNpCmpNpIdInOrder(&low, &high, &order) == 0 && order == -1, "reserved bytes did not order the NpIds"); + ok &= expect(sceNpCmpNpIdInOrder(&high, &low, &order) == 0 && order == 1, "the reverse order is not 1"); + ok &= expect(sceNpCmpNpIdInOrder(&low, &low, &order) == 0 && order == 0, "an NpId is not equal to itself"); + high = low; + high.opt[0] = 9; + high.reserved[0] = 0; + low.reserved[0] = 200; + ok &= expect(sceNpCmpNpIdInOrder(&low, &high, &order) == 0 && order == -1, "opt did not take precedence over reserved"); + const NpIdFull heronId = npIdWithTag("heron-4", 0); + ok &= expect(sceNpCmpNpIdInOrder(&heronId, &low, &order) == 0 && order == -1, "the handle did not take precedence"); + + const NpIdFull none = npIdWithTag("heron-4", 0); + const NpIdFull ps3 = npIdWithTag("heron-4", 0x00337370); + const NpIdFull vita = npIdWithTag("heron-4", 0x32707370); + const NpIdFull ps4 = npIdWithTag("heron-4", 0x00347370); + const NpIdFull other = npIdWithTag("heron-4", 0x00357370); + ok &= expect(sceNpGetPlatformType(nullptr) == INVALID_ARGUMENT, "a null NpId has a platform"); + ok &= expect(sceNpGetPlatformType(&none) == 0, "an untagged NpId is not platform 0"); + ok &= expect(sceNpGetPlatformType(&ps3) == 1, "the ps3 tag is not platform 1"); + ok &= expect(sceNpGetPlatformType(&vita) == 2, "the psp2 tag is not platform 2"); + ok &= expect(sceNpGetPlatformType(&ps4) == 3, "the ps4 tag is not platform 3"); + ok &= expect(sceNpGetPlatformType(&other) == INVALID_PLATFORM_TYPE, "an unknown tag was accepted"); + + NpOnlineId unterminated = onlineId("abcdefghijklmnop"); + unterminated.data[16] = 'q'; + ok &= expect(sceNpIntIsValidOnlineId(nullptr) == 0, "a null online ID is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&kestrel) == 1, "kestrel_77 is not valid"); + ok &= expect(sceNpIntIsValidOnlineId(&heron) == 1, "heron-4 is not valid"); + const NpOnlineId sixteen = onlineId("abcdefghijklmnop"); + const NpOnlineId shortId = onlineId("ab"); + const NpOnlineId leadingDash = onlineId("-heron"); + const NpOnlineId dotted = onlineId("her.on"); + ok &= expect(sceNpIntIsValidOnlineId(&sixteen) == 1, "a 16 character ID is not valid"); + ok &= expect(sceNpIntIsValidOnlineId(&unterminated) == 0, "an unterminated ID is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&shortId) == 0, "a 2 character ID is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&leadingDash) == 0, "an ID starting with '-' is valid"); + ok &= expect(sceNpIntIsValidOnlineId(&dotted) == 0, "an ID with '.' is valid"); + return ok ? 0 : 1; +} + +static void* APS5_VABI returnTicket(void* arg) { + return reinterpret_cast(reinterpret_cast(arg) + 1); +} + +static int testSync() { + PthreadMutex plain = nullptr; + PthreadMutex recursive = nullptr; + bool ok = expect(sceNpMutexInit(&plain, "np-plain", 0) == 0 && plain, "a plain mutex was not created"); + ok &= expect(sceNpLwMutexInit(&recursive, "np-recursive", 1) == 0 && recursive, "a recursive mutex was not created"); + if (!ok) return 1; + ok &= expect(sceNpMutexLock(&plain) == 0, "the plain mutex did not lock"); + ok &= expect(sceNpMutexTryLock(&plain) == MUTEX_BUSY, "a held plain mutex did not report BUSY"); + ok &= expect(sceNpMutexUnlock(&plain) == 0, "the plain mutex did not unlock"); + ok &= expect(sceNpLwMutexTryLock(&recursive) == 0 && sceNpLwMutexTryLock(&recursive) == 0, "flag 1 did not make the mutex recursive"); + ok &= expect(sceNpLwMutexUnlock(&recursive) == 0 && sceNpLwMutexUnlock(&recursive) == 0, "the recursive mutex did not unlock twice"); + ok &= expect(sceNpLwMutexDestroy(&recursive) == 0, "the recursive mutex was not destroyed"); + + PthreadCond cond = nullptr; + ok &= expect(sceNpCondInit(&cond, "np-cond", 0) == 0 && cond, "a condition was not created"); + ok &= expect(sceNpMutexLock(&plain) == 0, "the plain mutex did not lock for the wait"); + ok &= expect(sceNpCondTimedwait(&cond, &plain, 2000) == COND_TIMEDOUT, "a timed wait did not report TIMEDOUT"); + ok &= expect(sceNpMutexUnlock(&plain) == 0, "the mutex was not held after the wait"); + ok &= expect(sceNpCondSignal(&cond) == 0, "signalling without waiters failed"); + ok &= expect(sceNpCondDestroy(&cond) == 0, "the condition was not destroyed"); + ok &= expect(sceNpMutexDestroy(&plain) == 0, "the plain mutex was not destroyed"); + + Pthread thread = nullptr; + void* retval = nullptr; + ok &= expect(scePthreadCreate(&thread, nullptr, returnTicket, reinterpret_cast(0x4140), "np-join") == 0, "the thread was not created"); + ok &= expect(sceNpJoinThread(thread, &retval) == 0 && retval == reinterpret_cast(0x4141), "the join did not return the thread result"); + return ok ? 0 : 1; +} + +static int testClock() { + KernelTimespec before{}; + KernelTimespec after{}; + std::int64_t usec = -1; + sceKernelClockGettime(4, &before); + bool ok = expect(sceNpGetSystemClockUsec(&usec) == 0, "the clock was not read"); + sceKernelClockGettime(4, &after); + ok &= expect(usec >= before.tv_sec * 1000000 + before.tv_nsec / 1000 && usec <= after.tv_sec * 1000000 + after.tv_nsec / 1000, "the clock is not the monotonic kernel clock in microseconds"); + char version[12]; + std::memset(version, 'x', sizeof(version)); + sceNpGetSdkVersion(version); + ok &= expect(std::strcmp(version, "1.00") == 0, "the version is not 1.00"); + ok &= expect(version[8] == 'x', "the version was written past 8 bytes"); + return ok ? 0 : 1; +} + +int main(int argc, char** argv) { + if (argc != 2) return 2; + int status = 2; + if (std::strcmp(argv[1], "ids") == 0) status = testIds(); + else if (std::strcmp(argv[1], "sync") == 0) status = testSync(); + else if (std::strcmp(argv[1], "clock") == 0) status = testClock(); + if (status == 0) std::printf("NpCommon %s tests passed\n", argv[1]); + return status; +} From e0df782d2c993ec07bd11ea0b425ddffee203d10 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Wed, 21 Oct 2026 13:19:04 +0300 Subject: [PATCH 119/318] docs(debt): note where the NpCommon utilities and wrappers come from --- docs/dev/debt/libSceNpCommon.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 0429e246b8..e6e51903bb 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,7 +2,7 @@ ### Unknown function info -The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +The 129 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. - [sceNpAllocateKernelMemoryNoAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MWPOkqzYss0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpAllocateKernelMemoryWithAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `gMlY6eewr-c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -133,3 +133,9 @@ The 146 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpUtilJsonEscape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EftEB4kmkSg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vj04qzp7uKY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilJsonUnescape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4YJ5gYtRAAE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. + +### Functional + +- [Online ID and NpId utilities](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - `sceNpCmpOnlineId`, `sceNpCmpNpIdInOrder`, `sceNpGetPlatformType` and `sceNpIntIsValidOnlineId` are not measured on a console; their signatures, checks and results follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp), including the platform tags read from `opt[4..7]` and the 3 to 16 character online ID rule. +- [Mutex, condition and thread wrappers](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - the `sceNpMutex*`, `sceNpLwMutex*`, `sceNpCond*` and `sceNpJoinThread` wrappers forward to the libkernel `scePthread*` calls as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp) and are not measured on a console. Only `EBUSY` from try-lock and `ETIMEDOUT` from a timed wait are mapped to NP errors; every other kernel error is returned unchanged. The `flags` argument of `sceNpCondInit` is ignored. +- [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - writes the libkernel system software version as `major.minor` into an 8-byte buffer without checking it, as shadPS4 does; that version is the fixed `1.00`, not the SDK version a title was built with. From baa0ca3dfc200ff88f00d2034c4b262557af6e8d Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Wed, 21 Oct 2026 12:46:19 +0200 Subject: [PATCH 120/318] fix(agc): store a small label behind copied write-backs at reap instead of draining The first commit drained the device for every label recorded while any copied writer was listed, including labels nowhere near it. CPU-copied writers also occur in default runs, so that could cost a drain per label in normal play. While a copied writer is listed, a label of 8 bytes or less (every RELEASE_MEM, the fence WRITE_DATAs) is now not recorded on the GPU at all. A completion action of the open (or newest in-flight) batch stores it when that batch is reaped (AfterCompletions with storedOnGpu false, reason 5). Completions run in push order and batches finish front-first, so it lands after every earlier write-back without a drain. The existing GPU store plus completion re-store path would not keep this order: its GPU store is visible to a polling title before the write-back runs, and its re-store runs only over a write-back that overlapped the label itself. Recorder::PendingCompletionStores counts the guest stores completions still have to make (not end-of-pipe deliveries or write-backs). While it is not zero, later small labels follow the same route even after every writer was written back, so a later GPU label cannot become visible before an earlier completion store or land under it at one address. Stores over 8 bytes keep the GPU store and go behind the completions only over a copied writer's range or a pending completion store of their own range. Queue 0 reaps while it spins on REWIND, so a title polling such a label there cannot wait forever. The debt entry lists what this still leaves out of order (larger stores and GPU-made DMA_DATA copies away from those ranges, and a nested sync from inside a write-back). APS5_DRAIN_COMPLETION_LABELS=1 brings back the drain (reason 2); a second ctest entry runs the test with it. CopiedWriterLabelOrder now skips with a line naming minStorageBufferOffsetAlignment when the device binds the record at +0x1004 in place (alignment 4, AMD's driver and RADV) and nothing is written back. Above 4 the write-back count still fails the test. A new pass checks the order across batches: a later label to the address of a pending completion store is not visible before it and is not overwritten by it at the reap. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../Execution/include/VulkanDevice.hpp | 8 +- .../src/Driver/Queues/Submission.cpp | 1 + .../Execution/src/VulkanDevice.cpp | 36 +++++- .../Graphics/include/Recorder.hpp | 2 + .../libSceAgcDriver/Graphics/src/Recorder.cpp | 12 ++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 10 +- .../execution/CopiedWriterLabelOrder.cpp | 117 ++++++++++++++++-- docs/dev/ENVIRONMENT.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 2 +- 10 files changed, 175 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index adcd1d85a0..521390800f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2065,6 +2065,11 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_copied_writer_label_order_drain COMMAND agc_driver_copied_writer_label_order_tests) + set_tests_properties(agc_driver_copied_writer_label_order_drain PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAIN_COMPLETION_LABELS=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_copied_writer_label_order_drain PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores) set_tests_properties(agc_driver_multisample_readback PROPERTIES TIMEOUT 20) foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 1b5e098a87..3779854cbb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -92,10 +92,10 @@ public: // Records a store of `bytes` at a guest address behind the recorded work, so the value appears // once that work completed; the batch is submitted by the queue worker's flush rules (or at once // with APS5_LABEL_SUBMIT_NOW=1). `stamp` is the record-order stamp and `queue` the recording - // queue for the recorder's pending-label table. Returns 0 when recorded on the GPU, 5 when kept - // as a completion action behind pending write-backs, else why the CPU must write it: - // 1 nothing recorded (the write is already ordered), 2 write-backs pending and 3 memory not - // imported, 4 unsuitable size or alignment. + // queue for the recorder's pending-label table. Returns 0 when recorded on the GPU, 5 when stored + // by a completion behind pending CPU write-backs or completion stores, else why the CPU must + // write it: 1 nothing recorded (the write is already ordered), 2 write-backs pending (only with + // APS5_DRAIN_COMPLETION_LABELS=1) and 3 memory not imported, 4 unsuitable size or alignment. // `reapFirst` retires finished batches before the checks; a caller recording a group of labels // under one lock passes it for the first label only. int WriteLabelOnGpu(std::uint64_t address, std::span bytes, std::uint64_t stamp, std::uint32_t queue, bool reapFirst = true); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index 13d45c0f36..5249610775 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -282,6 +282,7 @@ void Driver::executeRewindTail(const Submission& stalled) { checkStopping(); noteAwaitingTitle(stalled.queue, reinterpret_cast(stalled.rewindTail - 1)); stall.Check("REWIND at", reinterpret_cast(stalled.rewindTail - 1)); + if (stalled.queue == 0 && completionsPending()) reapCompletionLabels(); PollSleep(); } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 76a12cd929..78380622d4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -2264,12 +2264,38 @@ int VulkanDevice::WriteLabelOnGpu(std::uint64_t address, std::spanbuffer, Graphics::HostImportOffset(*import, address), bytes, address); }; - // Only a write-back that can land over these bytes (a listed copied writer of the range, as - // FillBuffer tests) puts the label behind the completions; any other pending completion - // stores elsewhere. Debug aid: APS5_LABEL_GATE_ALL=1 gates on any completion, as before (a - // completion label there registered its own completion, so every later label followed). + // On the console every store a queue made before a label is in memory once the label is + // visible. A listed copied writer (a dispatch or draw whose written buffer the device holds as a + // CPU copy) is written back by the CPU when its batch is reaped, after the GPU already ran any + // store recorded here, so a title polling the label could rewrite the written region and have + // the late write-back land over its bytes (a title that rebuilds its command buffer there sees + // a stale packet). While one is listed, a label of 8 bytes or less (every RELEASE_MEM, the fence + // WRITE_DATAs) is therefore not recorded on the GPU at all: a completion action of the open (or + // newest in-flight) batch stores it when that batch is reaped. Completions run in push order and + // batches finish front-first, so it lands after every earlier write-back, whatever range that + // one wrote. The GPU store plus completion re-store below would not do: its GPU store is + // visible before the write-back, and its re-store runs only over a write-back that overlapped + // the label. While such a completion store is pending (PendingCompletionStores: no end-of-pipe + // delivery and no write-back counts there), later small labels follow it even after every + // writer was written back, or a GPU store would become visible before the earlier completion + // store lands (#1458), or land under it at the reap when both are at one address. Larger stores + // (DUMP_CONST_RAM, COPY_DATA and DMA_DATA payloads that later GPU work reads through the import) + // stay on the GPU and follow only a pending completion store over their own range. Readers of a + // range stored this way sync through its batch first (the label is a pending write of its batch + // and counted in PendingCompletionLabels). Debug aid: APS5_DRAIN_COMPLETION_LABELS=1 drains + // (reason 2) instead and stores the label on the CPU. + const bool listed = !state->copiedWriters->empty() || !Graphics::DrawCopiedWriters()->empty(); + if (drain) { + if (listed) return 2; + } else if (const bool storesPending = Graphics::Recorder::PendingCompletionStores() != 0; bytes.size() <= 8 ? (listed || storesPending) : (storesPending && recorder.CompletionLabelIn(address, bytes.size()))) { + recorder.AfterCompletions(address, bytes, stamp, queue, false); + return 5; + } + // A larger store over a listed copied writer's range (as FillBuffer tests it) goes behind the + // completions too, below; any other one stores on the GPU. Debug aid: APS5_LABEL_GATE_ALL=1 + // gates on any completion, as before (a completion label there registered its own completion, + // so every later label followed). static const bool gateAll = std::getenv("APS5_LABEL_GATE_ALL") != nullptr; - if (!state->copiedWriters->empty() || !Graphics::DrawCopiedWriters()->empty()) return 2; if (gateAll ? recorder.HasCompletions() : state->CopiedWriterOverlaps(address, bytes.size())) { // A copied buffer's write-back is still to run: the store must land after it. It is stored // by a completion action of the batch (a CPU memcpy when the batch is reaped) instead of diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index f4d473d7a5..41c6e67032 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -399,6 +399,7 @@ public: static std::uint64_t PublishGeneration(); // labels waiting in completion actions (the CPU must reap their batch for them to land); static std::uint64_t PendingCompletionLabels(); + static std::uint64_t PendingCompletionStores(); // write-back completions (OnComplete) of unfinished batches: a reaper retires them when every // worker sleeps, as for the labels above; static std::uint64_t PendingWriteBackCompletions(); @@ -596,6 +597,7 @@ private: // Labels stored by this batch's completion actions (see AfterCompletions); subtracted from // the lock-free pending count when the batch finishes, whether or not its completions ran. std::uint32_t completionLabelCount = 0; + std::uint32_t completionStoreCount = 0; // Write-back completions (OnComplete), likewise. std::uint32_t writeBackCompletionCount = 0; // The ranges of the completion labels also stored on the GPU, and whether each entered the diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 6655a45e82..83c8381e36 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -513,6 +513,7 @@ std::atomic pendingLabelSince{NoPendingLabel}; std::atomic writeGeneration{0}; std::atomic publishGeneration{0}; std::atomic completionLabels{0}; +std::atomic completionStores{0}; std::atomic writeBackCompletions{0}; std::atomic completionStoresSkipped{0}; std::atomic completionStoresRun{0}; @@ -1135,6 +1136,7 @@ Recorder::~Recorder() { publishGeneration.fetch_add(1, std::memory_order_release); pendingLabelSince.store(NoPendingLabel, std::memory_order_release); completionLabels.store(0, std::memory_order_release); + completionStores.store(0, std::memory_order_release); writeBackCompletions.store(0, std::memory_order_release); workSinceSubmit.store(0, std::memory_order_release); } @@ -1210,6 +1212,10 @@ std::uint64_t Recorder::PendingCompletionLabels() { return completionLabels.load(std::memory_order_acquire); } +std::uint64_t Recorder::PendingCompletionStores() { + return completionStores.load(std::memory_order_acquire); +} + std::uint64_t Recorder::PendingWriteBackCompletions() { return writeBackCompletions.load(std::memory_order_acquire); } @@ -3335,6 +3341,8 @@ void Recorder::AfterCompletions(std::uint64_t address, std::spanmarkBehindCompletion(batch, label.begin, label.end); } }; @@ -3861,6 +3871,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool list.erase(std::remove(list.begin(), list.end(), batch), list.end()); // The batch's completion labels have landed, or never will (fence failure below). if (batch->completionLabelCount != 0) completionLabels.fetch_sub(batch->completionLabelCount, std::memory_order_acq_rel); + if (batch->completionStoreCount != 0) completionStores.fetch_sub(batch->completionStoreCount, std::memory_order_acq_rel); if (batch->writeBackCompletionCount != 0) writeBackCompletions.fetch_sub(batch->writeBackCompletionCount, std::memory_order_acq_rel); // This also runs while the fence-failure throw unwinds: an allocation failure in the // rebuild keeps the previous snapshot, which is a superset (a stale entry only causes a @@ -3994,6 +4005,7 @@ void Recorder::finish(std::unique_ptr batch, bool wait, int source, bool void Recorder::discard(std::unique_ptr batch) noexcept { removeLabels(*batch); if (batch->completionLabelCount != 0) completionLabels.fetch_sub(batch->completionLabelCount, std::memory_order_acq_rel); + if (batch->completionStoreCount != 0) completionStores.fetch_sub(batch->completionStoreCount, std::memory_order_acq_rel); if (batch->writeBackCompletionCount != 0) writeBackCompletions.fetch_sub(batch->writeBackCompletionCount, std::memory_order_acq_rel); if (activeRecorder == this) pendingLabelSince.store(NoPendingLabel, std::memory_order_release); release(*batch); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a5322a1921..168e4d958d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -1109,8 +1109,10 @@ void completionCountTests(const Device& device, Recorder& recorder) { recorder.AfterCompletions(base, value, 3, 0, true); recorder.AfterCompletions(base + 8, value, 4, 0, true); Require(Recorder::PendingCompletionLabels() == registered, "a GPU-stored completion label counts before any write-back"); + Require(Recorder::PendingCompletionStores() == registered, "a GPU-stored completion label counts as a completion store before any write-back"); recorder.AfterCompletions(base + 0x40, value, 5, 0, false); Require(Recorder::PendingCompletionLabels() == registered + 1, "a label the GPU has no view of is not pending"); + Require(Recorder::PendingCompletionStores() == registered + 1, "a label the GPU has no view of is not a pending completion store"); // A late lookup (stamp <= afterStamp) of a label whose value reaches memory only through the // batch's completion action is refused as BehindCompletion (a poller reaps for it); a GPU-stored // one keeps the ordinary reason (unclosed group, or trust off) until a write-back overlaps it. @@ -1126,16 +1128,20 @@ void completionCountTests(const Device& device, Recorder& recorder) { Recorder::NoteWrittenBack(base + 2, 4); const std::uint64_t counted = countAll ? 3 : 2; Require(Recorder::PendingCompletionLabels() == counted, "a write-back over a GPU-stored label did not count it exactly once"); + Require(Recorder::PendingCompletionStores() == counted, "a write-back over a GPU-stored label did not make it a completion store exactly once"); + int delivered = 0; + Require(recorder.AfterRecordedWork([&] { ++delivered; }), "an action after recorded work was refused"); + Require(Recorder::PendingCompletionLabels() == counted + 1 && Recorder::PendingCompletionStores() == counted, "an action after recorded work counted as a completion store"); Require(!Recorder::LookupLabel(base, 4, 3, &refusal).has_value() && refusal == Refusal::BehindCompletion, "a written-back GPU-stored label is not refused as behind a completion"); Require(!Recorder::LookupLabel(base + 8, 4, 4, &refusal).has_value() && refusal == (countAll ? Refusal::BehindCompletion : ordinary), "an untouched GPU-stored label became behind a completion"); recorder.Submit(); device.WaitQueue(); - Require(Recorder::PendingCompletionLabels() == counted && Recorder::PendingWriteBackCompletions() == 1, "counts dropped before the batch finished"); + Require(Recorder::PendingCompletionLabels() == counted + 1 && Recorder::PendingCompletionStores() == counted && Recorder::PendingWriteBackCompletions() == 1, "counts dropped before the batch finished"); memory[0] = 7; memory[2] = 7; memory[16] = 7; recorder.Sync(); - Require(ran == 1 && Recorder::PendingCompletionLabels() == 0 && Recorder::PendingWriteBackCompletions() == 0 && recorder.Idle(), "counts did not return to 0 at finish"); + Require(ran == 1 && delivered == 1 && Recorder::PendingCompletionLabels() == 0 && Recorder::PendingCompletionStores() == 0 && Recorder::PendingWriteBackCompletions() == 0 && recorder.Idle(), "counts did not return to 0 at finish"); const bool storeAlways = std::getenv("APS5_LABEL_STORE_ALWAYS") != nullptr; Require(memory[0] == 1 && memory[2] == (storeAlways ? 1u : 7u) && memory[16] == 1, "completion stores ran for the wrong labels (overlapped and not-imported ones store, the untouched GPU-stored one skips)"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp index 3f7a722c88..f143769c82 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/CopiedWriterLabelOrder.cpp @@ -100,31 +100,73 @@ void RecordDispatch(AgcDriver::VulkanDevice& device, std::uint64_t record) { device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); } -bool LabelPass(AgcDriver::VulkanDevice& device, const GuestBlock& records, std::size_t recordOffset, const GuestBlock& labels, std::size_t labelOffset, std::uint64_t stamp, std::uint64_t writeBacks, int expectedReason, const std::string& what) { +bool Drains() { + return std::getenv("APS5_DRAIN_COMPLETION_LABELS") != nullptr; +} + +VkDeviceSize StorageAlignment(AgcDriver::VulkanDevice& device) { + return device.graphicsContext().limits.minStorageBufferOffsetAlignment; +} + +std::array LabelBytes(std::uint32_t value) { + std::array bytes{}; + std::memcpy(bytes.data(), &value, sizeof(value)); + return bytes; +} + +void WaitForGpu(AgcDriver::VulkanDevice& device, std::uint64_t epoch) { + if (device.CanWaitUnlocked()) device.WaitRecorded(epoch); + else std::this_thread::sleep_for(std::chrono::milliseconds(50)); +} + +std::uint64_t SubmitLocked(AgcDriver::VulkanDevice& device) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + return device.SubmitAndEpoch(); +} + +int LabelPass(AgcDriver::VulkanDevice& device, const GuestBlock& records, std::size_t recordOffset, const GuestBlock& labels, std::size_t labelOffset, std::uint64_t stamp, std::uint64_t writeBacks, int expectedReason, const std::string& what) { volatile std::uint32_t* stored = records.Word(recordOffset + StoredOffset); volatile std::uint32_t* label = labels.Word(labelOffset); *stored = CpuValue; *label = 0; RecordDispatch(device, records.Address() + recordOffset); const auto pending = Recorder::PendingWriteBackCompletions(); - Require(pending == writeBacks, what + ": " + std::to_string(pending) + " write-back completions are pending after the dispatch, expected " + std::to_string(writeBacks) + " (the test needs APS5_CPU_COPIES=1 and a storage buffer offset alignment above 4)"); - const std::array bytes{std::byte{LabelValue}, std::byte{0}, std::byte{0}, std::byte{0}}; + if (writeBacks != 0 && pending == 0 && recordOffset % StorageAlignment(device) == 0) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.WaitIdle(); + return VulkanTestSkipped; + } + Require(pending == writeBacks, what + ": " + std::to_string(pending) + " write-back completions are pending after the dispatch, expected " + std::to_string(writeBacks)); + const auto bytes = LabelBytes(LabelValue); int reason = 0; + std::uint64_t epoch = 0; { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); reason = device.WriteLabelOnGpu(labels.Address() + labelOffset, bytes, stamp, 0, false); if (reason == 3) { device.WaitIdle(); - return false; + return 1; } Require(reason == expectedReason, what + ": the label after the dispatch was handled with reason " + std::to_string(reason) + ", expected " + std::to_string(expectedReason)); if (reason >= 1 && reason <= 4) { if (reason != 1) device.WaitIdle(); AgcDriver::GuestMemory::Write(labels.Address() + labelOffset, bytes, 4); + } else if (reason == 5) { + Require(Recorder::PendingWriteBackCompletions() == 1 && Recorder::PendingCompletionStores() == 1, what + ": the label behind the write-back drained the device or is not a pending completion store"); + const std::array wide{}; + Require(device.WriteLabelOnGpu(labels.Address() + labelOffset + 0x100, wide, stamp, 0, false) == 0, what + ": a 16-byte store away from the copied writer and the pending completion store was not recorded on the GPU"); + epoch = device.SubmitAndEpoch(); } else { device.SubmitRecorded(true); } } + if (reason == 5) { + WaitForGpu(device, epoch); + Require(*label == 0, what + ": the label is visible before the batch that writes back the dispatch's record was reaped"); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.ReapRecorded(epoch); + Require(Recorder::PendingCompletionStores() == 0 && Recorder::PendingWriteBackCompletions() == 0, what + ": completions outlived the reap of their batch"); + } const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (*label != LabelValue) { Require(std::chrono::steady_clock::now() < deadline, what + ": the label never landed"); @@ -133,11 +175,63 @@ bool LabelPass(AgcDriver::VulkanDevice& device, const GuestBlock& records, std:: const std::uint32_t seen = *stored; Require(seen == GpuValue, what + ": the label is visible but the record holds " + Hex(seen) + ", the dispatch stored " + Hex(GpuValue)); *stored = CpuValue; - device.WaitIdle(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.WaitIdle(); + } const std::uint32_t kept = *stored; Require(kept == CpuValue, what + ": the record holds " + Hex(kept) + " after the drain, a write-back landed over the value the CPU wrote after the label (" + Hex(CpuValue) + ")"); Require(Recorder::PendingWriteBackCompletions() == 0, what + ": write-back completions outlived the drain"); - return true; + return 0; +} + +void StreamOrderPass(AgcDriver::VulkanDevice& device, const GuestBlock& copied, const GuestBlock& direct, const GuestBlock& labels, std::uint64_t stamp) { + constexpr std::uint32_t First = 0x11u; + constexpr std::uint32_t Second = 0x22u; + const std::uint64_t address = labels.Address() + 0x4000; + volatile std::uint32_t* label = labels.Word(0x4000); + *label = 0; + *copied.Word(CopiedRecordOffset + StoredOffset) = CpuValue; + RecordDispatch(device, copied.Address() + CopiedRecordOffset); + Require(Recorder::PendingWriteBackCompletions() == 1, "stream order: the copied dispatch has no pending write-back"); + const auto copiedEpoch = SubmitLocked(device); + RecordDispatch(device, direct.Address() + DirectRecordOffset); + std::uint64_t firstEpoch = 0; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(Recorder::PendingWriteBackCompletions() == 1, "stream order: the copied writer was written back before the first label"); + const int reason = device.WriteLabelOnGpu(address, LabelBytes(First), stamp, 0, false); + Require(reason == 5, "stream order: the first label behind the copied writer was handled with reason " + std::to_string(reason) + ", expected 5"); + firstEpoch = device.SubmitAndEpoch(); + } + WaitForGpu(device, firstEpoch); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.ReapRecorded(copiedEpoch); + Require(Recorder::PendingWriteBackCompletions() == 0, "stream order: the copied writer was not written back by the reap of its batch"); + Require(Recorder::PendingCompletionStores() == 1, "stream order: the first label's completion store is not pending after the copied writer's reap"); + Require(*label == 0, "stream order: the first label landed before its batch was reaped"); + } + RecordDispatch(device, direct.Address() + DirectRecordOffset); + std::uint64_t secondEpoch = 0; + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + const int reason = device.WriteLabelOnGpu(address, LabelBytes(Second), stamp + 1, 0, false); + Require(reason == 5, "stream order: a later label to the address of a pending completion store was handled with reason " + std::to_string(reason) + ", expected 5"); + secondEpoch = device.SubmitAndEpoch(); + Require(secondEpoch > firstEpoch, "stream order: the second label went into the first label's batch"); + } + WaitForGpu(device, secondEpoch); + const std::uint32_t early = *label; + Require(early != Second, "stream order: the later label is visible while the earlier completion store to its address has not landed"); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.ReapRecorded(firstEpoch); + Require(*label == First, "stream order: the label holds " + Hex(*label) + " after the first label's batch was reaped, expected " + Hex(First)); + device.ReapRecorded(secondEpoch); + const std::uint32_t last = *label; + Require(last == Second, "stream order: the label holds " + Hex(last) + " after the reap, the later label stored " + Hex(Second)); + Require(Recorder::PendingCompletionStores() == 0 && Recorder::PendingCompletionLabels() == 0, "stream order: completion stores outlived the reap of their batches"); + device.WaitIdle(); } } @@ -146,18 +240,27 @@ int main() { try { #ifdef _WIN32 _putenv_s("APS5_CPU_COPIES", "1"); + _putenv_s("APS5_NO_OPPORTUNISTIC_REAP", "1"); #else setenv("APS5_CPU_COPIES", "1", 1); + setenv("APS5_NO_OPPORTUNISTIC_REAP", "1", 1); #endif const GuestBlock copied; const GuestBlock direct; const GuestBlock labels; const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - if (!LabelPass(*device, copied, CopiedRecordOffset, labels, 0x2000, 1, 1, 2, "copied writer") || !LabelPass(*device, direct, DirectRecordOffset, labels, 0x3000, 2, 0, 0, "direct writer")) { + const int copiedPass = LabelPass(*device, copied, CopiedRecordOffset, labels, 0x2000, 1, 1, Drains() ? 2 : 5, "copied writer"); + if (copiedPass == VulkanTestSkipped) { + std::printf("skipped, the device reports a minStorageBufferOffsetAlignment of %llu, so the dispatch binds its record at +0x%zx in place and nothing is written back; the test needs an alignment above 4\n", + static_cast(StorageAlignment(*device)), CopiedRecordOffset); + return VulkanTestSkipped; + } + if (copiedPass != 0 || LabelPass(*device, direct, DirectRecordOffset, labels, 0x3000, 2, 0, 0, "direct writer") != 0) { std::puts("skipped, the device imports no host memory"); return VulkanTestSkipped; } + if (!Drains()) StreamOrderPass(*device, copied, direct, labels, 3); std::puts("copied writer label order tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index a309403888..8e8fc62713 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -111,7 +111,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DISPATCH_VARIANTS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/DispatchCache.cpp` | 4 | number of dispatch variants kept per cache entry, clamped to the maximum | | `APS5_DISPMOVE_MAX_KIB` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | 1024 | upper bound in KiB on the read-only buffers a dispatch-cache hit moves into its template, above which the dispatch is prepared again | | `APS5_DRAIN_ALL` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | drains every packet, disabling the batched recording path | -| `APS5_DRAIN_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records completion labels immediately instead of deferring them with the batch | +| `APS5_DRAIN_COMPLETION_LABELS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/DeferredLabels.cpp` | | records completion labels immediately instead of deferring them with the batch, and drains a label recorded while a copied write-back is pending instead of storing it behind the write-backs | | `APS5_DRAW_CACHE_ENTRIES` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCache.cpp` | 4096 | number of entries in the draw cache; 0 keeps the default 4096 | | `APS5_DRAW_DRAIN` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp` | | treats draw packets as drains, waiting for them instead of recording ahead | | `APS5_DRAW_INPUTS_IN_PLACE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | binds draw vertex and index ranges in place from host imports also with NVIDIA's Windows driver, which copies them by default | diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 79655753f4..610937c25a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -113,7 +113,7 @@ - A [COND_EXEC range](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. - A [COND_INDIRECT_BUFFER](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (`sceAgcCbBranch`) runs only with the always-pass compare function; a comparison or a predicated packet throws. Modes 1 and 2 run the first buffer, assumed to be the if-then and if-then-else modes of AMD's PM4 packet. [sceAgcCbBranch](../../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) does not check the compare address of an always-pass branch or the address of an empty buffer, as PPSA14632 passes an uninitialised compare address and a null else buffer of 0 dwords. - A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (`RELEASE_MEM`, `WRITE_DATA`, `DMA_DATA`) into guest memory the device has no host import of waits for all recorded work and is then written by the CPU, so the queue worker stalls once per such label. The console's CP stores it in stream order without that stall. -- A [label or store](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) recorded while a copied writer is listed (a dispatch or draw whose written buffer the device holds as a CPU copy, written back to guest memory when its batch is reaped) waits for all recorded work, which runs the write-back, and is then written by the CPU: on the console every earlier store is in memory before a later label of the same queue is visible, and a title that rewrites the written region after polling the label would otherwise have the bytes overwritten by the late write-back (Gang Beasts, CUSA04865 through #6977, rebuilt its command buffer over such a region). The stall is paid once per label while such a write-back is pending; written buffers the device imports in place bind directly and pay nothing. +- A [label](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) of 8 bytes or less recorded while a copied writer is listed, or while an earlier completion store is pending, is stored by the CPU when its batch is reaped, after the write-backs, so it becomes visible up to one reap later than on the console, also after GPU writes recorded behind it. Larger stores (DUMP_CONST_RAM, COPY_DATA, DMA_DATA) and DMA_DATA copies the GPU makes stay on the GPU unless they cover a copied writer's range or a pending completion store, so they can be visible before an unrelated earlier write-back. The order also breaks when a write-back's guest store syncs a pending storage image from inside the completion: that nested sync finishes later batches, whose completion stores land first. `APS5_DRAIN_COMPLETION_LABELS=1` drains instead (reason 2). - [WRITE_DATA to thread-trace user-data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) updates the queue's UCONFIG values for register destinations `0xc340..0xc347`, including repeated writes to one address; these are not guest-memory labels. The range follows AMD's GFX10 `SQ_THREAD_TRACE_USERDATA_0..7` definitions in [the Linux register header](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/include/asic_reg/gc/gc_10_1_0_offset.h) and the packets emitted by PPSA21564's bundled AGC driver before its EOP flip. Other register destinations throw. GPU thread-trace capture and its user-data stream are not emitted; only the final register state is retained. Console trace behavior is unverified. - [WRITE_DATA with DST_SEL 3](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver) writes guest memory like DST_SEL 5. AMD's GFX10 header names 3 the GDS, but Killzone: Liberation (PPSA11142) writes its frame fence with `sceAgcDcbWriteData(dst 6, cache policy 3, write confirm)`, which the SPRX-confirmed encoding ([OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/cb_builders.c)) turns into DST_SEL 3 with a 40-bit label address in the title's own memory that `RELEASE_MEM` and `WAIT_REG_MEM` then use; the console must write that label or the title would wait on it forever. Whether the console routes it through the GDS or a cache is unknown - [SET_*_REG_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) reads its register list when the command buffer is submitted, with the packets. The console's graphics PFP reads it when it reaches the packet, so a list written after the submission (by the CPU, or by GPU work ahead of the packet behind PFP_SYNC_ME) is read here before that write. From 736ee67caf96b78295e9563f6c790ffb47a4cba9 Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 00:23:37 +1300 Subject: [PATCH 121/318] feat(libSceNpCommon): implement 94 more of the module's exports Builds on #9037. Adds threads, event flags, semaphores, atomics, heaps and kernel memory, callout contexts, time and SDK version queries, base64 and hex codecs, integer and string helpers, JSON escaping, URL encoding, online id strings, language codes, title ids and debug output. The behaviour follows libSceNpCommon.sprx from firmware 12.00, read from disassembly. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/CMakeLists.txt | 11 + core/libs/prx/libSceNpCommon/CMakeLists.txt | 2 + .../prx/libSceNpCommon/PendingExports.cpp | 470 ---------- core/libs/prx/libSceNpCommon/Runtime.cpp | 836 ++++++++++++++++++ core/libs/prx/libSceNpCommon/Strings.cpp | 629 +++++++++++++ core/libs/tests/GuestNpCommonRuntime.cpp | 495 +++++++++++ docs/dev/debt/libSceNpCommon.md | 106 +-- 7 files changed, 1984 insertions(+), 565 deletions(-) create mode 100644 core/libs/prx/libSceNpCommon/Runtime.cpp create mode 100644 core/libs/prx/libSceNpCommon/Strings.cpp create mode 100644 core/libs/tests/GuestNpCommonRuntime.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index a1e0d5218d..36386a8f15 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -393,6 +393,17 @@ if(BUILD_TESTING) endif() endforeach() + add_test_executable(guest_np_common_runtime_tests tests/GuestNpCommonRuntime.cpp) + target_include_directories(guest_np_common_runtime_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_common_runtime_tests PRIVATE libSceNpCommon libkernel libc) + configure_windows_unwind(guest_np_common_runtime_tests) + add_test(NAME guest_np_common_runtime COMMAND guest_np_common_runtime_tests) + set_tests_properties(guest_np_common_runtime PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_common_runtime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc) diff --git a/core/libs/prx/libSceNpCommon/CMakeLists.txt b/core/libs/prx/libSceNpCommon/CMakeLists.txt index f85817c30e..bab2481853 100644 --- a/core/libs/prx/libSceNpCommon/CMakeLists.txt +++ b/core/libs/prx/libSceNpCommon/CMakeLists.txt @@ -2,5 +2,7 @@ add_library(libSceNpCommon SHARED EXCLUDE_FROM_ALL PendingExports.cpp) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp") target_sources(libSceNpCommon PRIVATE Export.cpp) endif() +target_sources(libSceNpCommon PRIVATE Runtime.cpp Strings.cpp) target_include_directories(libSceNpCommon PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(libSceNpCommon PRIVATE libc libkernel libSceRtc libSceUserService) configure_windows_unwind(libSceNpCommon) diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index 4326061d26..30a117b230 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -5,106 +5,6 @@ extern "C" { -int APS5_VABI sceNpAllocateKernelMemoryNoAlignment(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpAllocateKernelMemoryWithAlignment(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpArchInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpArchTerm(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpAtomicCas32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpAtomicDec32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpAtomicInc32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64Decoder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64Encoder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64GetDecodeSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64UrlDecoder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64UrlEncoder(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpBase64UrlGetDecodeSize(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCalloutInitCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCalloutStartOnCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCalloutStartOnCtx64(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCalloutStopOnCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCalloutTermCtx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCancelEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpClearEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpCloseEventFlag(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -115,176 +15,21 @@ int APS5_VABI sceNpCloseSema(void) { return 0; } -int APS5_VABI sceNpCondSignalAll(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCondSignalTo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCondWait(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCreateEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCreateSema(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpCreateThread(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpDbgAssignDebugId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpDbgDumpBinary(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpDbgDumpText(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpDeleteEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpDeleteSema(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpFreeKernelMemory(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpGetNavSdkVersion(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpGetProcessId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGetRandom(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGetSdkVersionUInt(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGlobalHeapGetAllocator(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGlobalHeapGetAllocatorEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGlobalHeapGetAllocatorExPtr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpGlobalHeapGetAllocatorPtr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHeapDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHeapGetAllocator(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHeapGetStat(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHeapInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHeapShowStat(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpHexToInt(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpInt32ToStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpInt64ToStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpIntGetPlatformType(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpIntIsOnlineIdString(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpIntSetPlatformType(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpIntToHex(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpIpc2ClientInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpIpc2ClientTerm(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpJsonParse(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -315,96 +60,11 @@ int APS5_VABI sceNpJsonParseInit(void) { return 0; } -int APS5_VABI sceNpLwCondDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwCondInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwCondSignal(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwCondSignalAll(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwCondSignalTo(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpLwCondWait(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMemoryHeapDestroy(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMemoryHeapGetAllocator(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMemoryHeapGetAllocatorEx(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpMemoryHeapInit(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpOpenEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpOpenSema(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpPanic(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpPollEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpPollSema(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpRtcConvertToPosixTime(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpRtcFormatRFC3339(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpRtcParseRFC3339(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpServerErrorJsonGetErrorCode(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -430,91 +90,11 @@ int APS5_VABI sceNpServerErrorJsonParseMultiInit(void) { return 0; } -int APS5_VABI sceNpSetEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpSetPlatformType(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpSignalSema(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrBuildHex(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrParseHex(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrToInt32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrToInt64(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrToUInt32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrToUInt64(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrcpyToBuf(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrnParseHex(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpStrncpyToBuf(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpThreadGetId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUInt32ToStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUInt64ToStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUserGetUserIdList(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUtilBuildTitleId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpUtilCanonicalizeNpIdForPs4(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -525,11 +105,6 @@ int APS5_VABI sceNpUtilCanonicalizeNpIdForPsp2(void) { return 0; } -int APS5_VABI sceNpUtilCmpAccountId(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpUtilGetDateSetAuto(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -565,36 +140,11 @@ int APS5_VABI sceNpUtilGetNpDebug(void) { return 0; } -int APS5_VABI sceNpUtilGetNpLanguageCode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUtilGetNpLanguageCode2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUtilGetNpLanguageCode2Str(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUtilGetNpLanguageCodeStr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpUtilGetNpTestPatch(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpUtilGetNthChar(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpUtilGetShareTitleCheck(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -625,29 +175,9 @@ int APS5_VABI sceNpUtilGetWebTraceSetting(void) { return 0; } -int APS5_VABI sceNpUtilHttpUrlEncode(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceNpUtilJidToNpId(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI sceNpUtilJsonEscape(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpUtilJsonGetOneChar(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -} - -int APS5_VABI sceNpUtilJsonUnescape(void) { - NotImplemented_nid_no_patch(__func__); - return 0; } diff --git a/core/libs/prx/libSceNpCommon/Runtime.cpp b/core/libs/prx/libSceNpCommon/Runtime.cpp new file mode 100644 index 0000000000..1d87008ad5 --- /dev/null +++ b/core/libs/prx/libSceNpCommon/Runtime.cpp @@ -0,0 +1,836 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/MallocManagedSize.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/Pthread/include/Cond.hpp" +#include "prx/libkernel/Pthread/include/Mutex.hpp" +#include "prx/libkernel/Semaphore/include/Semaphore.hpp" + +extern "C" { +int APS5_VABI sceNpMutexInit(PthreadMutex* mutex, const char* name, std::uint64_t flags); +int APS5_VABI sceNpMutexDestroy(PthreadMutex* mutex); +int APS5_VABI sceNpMutexLock(PthreadMutex* mutex); +int APS5_VABI sceNpMutexUnlock(PthreadMutex* mutex); +int APS5_VABI sceNpCondInit(PthreadCond* cond, const char* name, std::uint64_t flags); +int APS5_VABI sceNpCondDestroy(PthreadCond* cond); +int APS5_VABI sceNpCondSignal(PthreadCond* cond); +int APS5_VABI sceNpCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, std::uint32_t usec); +int APS5_VABI sceNpJoinThread(Pthread thread, void** retval); +int APS5_VABI sceNpGetSystemClockUsec(std::int64_t* usec); +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +Pthread APS5_VABI scePthreadSelf(); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksize); +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inherit_sched); +int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); +int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, uint32_t attr, uint64_t init_pattern, const void* param); +int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); +int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name); +int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, uint64_t bit_pattern); +int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, uint64_t bit_pattern); +int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, uint64_t bit_pattern, uint32_t wait_mode, uint64_t* result_pat); +int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, uint64_t set_pattern, int* num_wait_threads); +int APS5_VABI getpid_nid_postfix(void); +int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time); +int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes); +int APS5_VABI sceRtcSetTick(RtcDateTime* time, const RtcTick* tick); +int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds); +int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(void* version); +int APS5_VABI sceKernelClockGettime(KernelClockid clock_id, KernelTimespec* tp); +int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list); +int32_t APS5_VABI sceKernelMapNamedFlexibleMemory(void** addr_in_out, size_t len, int prot, int flags, const char* name); +int APS5_VABI sceKernelMunmap(uint64_t vaddr, size_t len); +void* APS5_VABI sceLibcMspaceCreate_nid_postfix(const char* name, void* base, std::size_t size, unsigned flags); +int APS5_VABI sceLibcMspaceDestroy_nid_postfix(void* handle); +void* APS5_VABI sceLibcMspaceMalloc_nid_postfix(void* handle, std::size_t size); +void APS5_VABI sceLibcMspaceFree_nid_postfix(void* handle, void* pointer); +void* APS5_VABI sceLibcMspaceRealloc_nid_postfix(void* handle, void* pointer, std::size_t size); +int APS5_VABI sceLibcMspaceMallocStats_nid_postfix(void* handle, MallocManagedSize* stats); +} + +struct NpTitleIdParts { + char id[9]; + char reserved; + std::uint8_t index; +}; + +struct NpCallout { + NpCallout* next; + void (APS5_VABI *handler)(void* arg); + std::uint64_t deadline; + void* arg; + std::uint64_t reserved; +}; +static_assert(sizeof(NpCallout) == 0x28); + +struct NpCalloutContext { + Pthread thread; + PthreadMutex mutex; + PthreadCond cond; + int initialized; + int stopping; + NpCallout* pending; +}; +static_assert(sizeof(NpCalloutContext) == 0x28); + +struct NpHeap { + void* mspace; +}; + +struct NpHeapStat { + std::uint64_t maxSystemSize; + std::uint64_t maxInuseSize; + std::uint64_t currentInuseSize; +}; + +struct NpKernelMemory { + std::uint64_t size; + std::uint64_t reserved; + void* address; +}; +static_assert(sizeof(NpKernelMemory) == 0x18); + +struct NpMemoryHeap { + NpKernelMemory memory; + NpHeap heap; +}; +static_assert(sizeof(NpMemoryHeap) == 0x20); + +struct NpAllocator { + void* (APS5_VABI *allocate)(std::size_t size, void* context); + void* (APS5_VABI *reallocate)(void* pointer, std::size_t size, void* context); + void (APS5_VABI *release)(void* pointer, void* context); + void* context; +}; + +struct NpAllocatorEx { + NpAllocator allocator; + int outOfMemoryError; +}; + +namespace { + +constexpr int NP_ERROR_NULL_ARGUMENT = static_cast(0x80550003); +constexpr int NP_ERROR_IPC_ALREADY_INITIALIZED = static_cast(0x80559D00); +constexpr int NP_ERROR_IPC_NOT_INITIALIZED = static_cast(0x80559D01); +constexpr int NP_ERROR_ONLINE_ID_MISMATCH = static_cast(0x80550609); +constexpr std::size_t NP_ONLINE_ID_MAX = 16; +constexpr std::size_t NP_ONLINE_ID_MIN = 3; +constexpr int NP_ERROR_INVALID_ARGUMENT_UTIL = static_cast(0x80550601); +constexpr std::size_t NP_LANGUAGE_CODE_SIZE = 6; +constexpr std::size_t NP_LANGUAGE_CODE2_SIZE = 0x24; + +struct NpLanguageCodes { + const char* code; + const char* tag; +}; + +constexpr NpLanguageCodes NP_LANGUAGE_CODES[] { + {"ja", "ja-JP"}, {"en", "en-US"}, {"fr", "fr-FR"}, {"es", "es-ES"}, {"de", "de-DE"}, {"it", "it-IT"}, + {"nl", "nl-NL"}, {"pt", "pt-PT"}, {"ru", "ru-RU"}, {"ko", "ko-KR"}, {"zh-TW", "zh-Hant"}, {"zh-CN", "zh-Hans"}, + {"fi", "fi-FI"}, {"sv", "sv-SE"}, {"da", "da-DK"}, {"no", "no-NO"}, {"pl", "pl-PL"}, {"pt-BR", "pt-BR"}, + {"en-GB", "en-GB"}, {"tr", "tr-TR"}, {"es-MX", "es-419"}, {"ar", "ar-AE"}, {"fr-CA", "fr-CA"}, {"cs", "cs-CZ"}, + {"hu", "hu-HU"}, {"el", "el-GR"}, {"ro", "ro-RO"}, {"th", "th-TH"}, {"vi", "vi-VN"}, {"id", "id-ID"}, + {"uk", "uk-UA"}, +}; + +const NpLanguageCodes* FindLanguageCodes(int language) { + if (language < 0 || static_cast(language) >= std::size(NP_LANGUAGE_CODES)) return nullptr; + return &NP_LANGUAGE_CODES[language]; +} + +void CopyText(char* out, const char* text, std::size_t size) { + std::strncpy(out, text, size - 1); + out[size - 1] = '\0'; +} + +std::atomic g_debugIds {0}; + +constexpr int NP_UTIL_ERROR_BUFFER_TOO_SMALL = static_cast(0x80550602); +constexpr int NP_UTIL_ERROR_OUT_OF_RANGE = static_cast(0x8055060A); +constexpr std::size_t NP_TITLE_ID_SIZE = 13; +constexpr std::uint8_t NP_TITLE_ID_INDEX_MAX = 99; +constexpr std::size_t NP_DUMP_LINE_BYTES = 16; + +bool IsAsciiLetter(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); +} + +bool IsOnlineIdText(const char* text) { + const std::size_t length = std::strlen(text); + if (length < NP_ONLINE_ID_MIN || length > NP_ONLINE_ID_MAX || !IsAsciiLetter(text[0])) return false; + return std::all_of(text + 1, text + length, [](char c) { + return IsAsciiLetter(c) || (c >= '0' && c <= '9') || c == '_' || c == '-'; + }); +} + +constexpr int NP_ERROR_BUFFER_TOO_SMALL = static_cast(0x80558008); +constexpr std::size_t NP_SDK_VERSION_SIZE = 8; +constexpr std::size_t NP_RFC3339_BUFFER_MIN = 0x80; +constexpr std::size_t NP_USER_ID_LIST_SIZE = 20; +constexpr int USER_ID_INVALID = -1; +constexpr KernelClockid NP_SYSTEM_CLOCK = 4; + +struct SystemSwVersion { + std::uint64_t size; + char versionString[0x1C]; + std::uint32_t version; +}; +static_assert(sizeof(SystemSwVersion) == 0x28); + +std::uint32_t SystemVersion() { + SystemSwVersion version {}; + version.size = sizeof(version); + sceKernelGetSystemSwVersion_nid_postfix(&version); + return version.version; +} + +int BcdPair(std::uint32_t value, int shift) { + return static_cast((value >> (shift + 4)) & 0xF) * 10 + static_cast((value >> shift) & 0xF); +} + + +constexpr int NP_ERROR_OUT_OF_MEMORY = static_cast(0x80550005); +constexpr int NP_ERROR_NULL_OBJECT = static_cast(0x80558005); +constexpr int NP_ERROR_HEAP_CREATE = static_cast(0x8055800A); +constexpr std::uint64_t NP_KERNEL_MEMORY_ALIGNMENT = 0x4000; +constexpr int NP_KERNEL_MEMORY_PROTECTION = 3; +constexpr std::size_t NP_GLOBAL_HEAP_SIZE = 0x80000; +constexpr char NP_GLOBAL_HEAP_NAME[] = "NpCommonGlobalHeap"; +constexpr std::size_t NP_IPC_CLIENT_HEAP_SIZE = 0x10000; +constexpr char NP_IPC_CLIENT_HEAP_NAME[] = "NpCommonIpcClientHeap"; + +void* APS5_VABI HeapAllocate(std::size_t size, void* context) { + auto* heap = static_cast(context); + return heap->mspace != nullptr ? sceLibcMspaceMalloc_nid_postfix(heap->mspace, size) : nullptr; +} + +void* APS5_VABI HeapReallocate(void* pointer, std::size_t size, void* context) { + auto* heap = static_cast(context); + return heap->mspace != nullptr ? sceLibcMspaceRealloc_nid_postfix(heap->mspace, pointer, size) : nullptr; +} + +void APS5_VABI HeapRelease(void* pointer, void* context) { + auto* heap = static_cast(context); + if (heap->mspace != nullptr && pointer != nullptr) sceLibcMspaceFree_nid_postfix(heap->mspace, pointer); +} + +void* APS5_VABI MemoryHeapAllocate(std::size_t size, void* context) { + return HeapAllocate(size, &static_cast(context)->heap); +} + +void* APS5_VABI MemoryHeapReallocate(void* pointer, std::size_t size, void* context) { + return HeapReallocate(pointer, size, &static_cast(context)->heap); +} + +void APS5_VABI MemoryHeapRelease(void* pointer, void* context) { + HeapRelease(pointer, &static_cast(context)->heap); +} + +std::uint64_t AlignKernelMemory(std::uint64_t size) { + const std::uint64_t remainder = size & (NP_KERNEL_MEMORY_ALIGNMENT - 1); + return remainder == 0 ? size : size + NP_KERNEL_MEMORY_ALIGNMENT - remainder; +} + +int MapKernelMemory(NpKernelMemory* memory, std::uint64_t size, const char* name) { + memory->reserved = 0; + memory->size = size; + return sceKernelMapNamedFlexibleMemory(&memory->address, size, NP_KERNEL_MEMORY_PROTECTION, 0, name); +} + +void UnmapKernelMemory(NpKernelMemory* memory) { + if (memory->address != nullptr) sceKernelMunmap(reinterpret_cast(memory->address), memory->size); + memory->address = nullptr; +} + +int InitHeap(NpHeap* heap, void* base, std::size_t size, const char* name) { + heap->mspace = sceLibcMspaceCreate_nid_postfix(name, base, size, 0); + return heap->mspace != nullptr ? 0 : NP_ERROR_HEAP_CREATE; +} + +void DestroyHeap(NpHeap* heap) { + if (heap->mspace == nullptr) return; + sceLibcMspaceDestroy_nid_postfix(heap->mspace); + heap->mspace = nullptr; +} + +int InitMemoryHeap(NpMemoryHeap* heap, std::uint64_t size, const char* name) { + if (heap == nullptr) return NP_ERROR_NULL_OBJECT; + int result = MapKernelMemory(&heap->memory, AlignKernelMemory(size), name); + if (result < 0) return result; + result = InitHeap(&heap->heap, heap->memory.address, heap->memory.size, name); + if (result >= 0) return result; + UnmapKernelMemory(&heap->memory); + *heap = {}; + return result; +} + +void DestroyMemoryHeap(NpMemoryHeap* heap) { + DestroyHeap(&heap->heap); + UnmapKernelMemory(&heap->memory); +} + +NpAllocator MemoryHeapAllocator(NpMemoryHeap* heap) { + return {MemoryHeapAllocate, MemoryHeapReallocate, MemoryHeapRelease, heap}; +} + +constexpr int NP_ERROR_NOT_INITIALIZED = static_cast(0x80558001); +constexpr int NP_ERROR_ALREADY_INITIALIZED = static_cast(0x80558002); +constexpr int NP_ERROR_ALREADY_QUEUED = static_cast(0x80558006); +constexpr std::uint64_t NP_CALLOUT_MAX_WAIT = 0xFFFFFFFF; + +std::mutex g_randomMutex; +std::mt19937 g_random {std::random_device {}()}; + +NpMemoryHeap g_globalHeap {}; +NpAllocator g_globalAllocator {}; +NpAllocatorEx g_globalAllocatorEx {}; +std::atomic g_archReferences {0}; +std::mutex g_ipcClientMutex; +bool g_ipcClientInitialized = false; +NpMemoryHeap g_ipcClientHeap {}; + +constexpr int KERNEL_ERROR_EBUSY = static_cast(0x80020010); +constexpr int KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +constexpr int NP_ERROR_INVALID_ARGUMENT = static_cast(0x80558009); +constexpr int NP_ERROR_TIMEOUT = static_cast(0x8055800B); +constexpr int NP_ERROR_EVENT_FLAG_BUSY = static_cast(0x8055800C); +constexpr int NP_ERROR_SEMA_BUSY = static_cast(0x8055800D); +constexpr int NP_ERROR_MUTEX_BUSY = static_cast(0x8055800F); +constexpr std::uint32_t NP_MUTEX_FLAG_RECURSIVE = 1; +constexpr int PTHREAD_MUTEX_TYPE_RECURSIVE = 2; +constexpr std::uint32_t NP_SEMA_FLAG_PRIORITY = 1; +constexpr std::uint32_t KERNEL_SEMA_ATTR_FIFO = 1; +constexpr std::uint32_t KERNEL_SEMA_ATTR_PRIORITY_SHIFT = 8; +constexpr int NP_THREAD_EXPLICIT_SCHED = 0; +constexpr int NP_THREAD_SCHED_POLICY = 1; + +int NpResult(int result) { + return result < 0 ? result : 0; +} + +int NpBusyResult(int result, int busy) { + return result == KERNEL_ERROR_EBUSY ? busy : NpResult(result); +} + +int InitMutex(PthreadMutex* mutex, const char* name, std::uint32_t flags) { + PthreadMutexattr attr {}; + int result = scePthreadMutexattrInit(&attr); + if (result < 0) return result; + if ((flags & NP_MUTEX_FLAG_RECURSIVE) != 0) result = scePthreadMutexattrSettype(&attr, PTHREAD_MUTEX_TYPE_RECURSIVE); + if (result >= 0) result = scePthreadMutexInit(mutex, &attr, name); + scePthreadMutexattrDestroy(&attr); + return NpResult(result); +} + +int InitCond(PthreadCond* cond, const char* name) { + PthreadCondattr attr {}; + const int result = scePthreadCondattrInit(&attr); + if (result < 0) return result; + const int created = scePthreadCondInit(cond, &attr, name); + scePthreadCondattrDestroy(&attr); + return created; +} + +} + +extern "C" { + +int APS5_VABI sceNpAtomicCas32(std::int32_t* value, std::int32_t expected, std::int32_t desired) { + std::atomic_ref(*value).compare_exchange_strong(expected, desired); + return expected; +} + +int APS5_VABI sceNpAtomicInc32(std::int32_t* value) { + return std::atomic_ref(*value).fetch_add(1); +} + +int APS5_VABI sceNpAtomicDec32(std::int32_t* value) { + return std::atomic_ref(*value).fetch_sub(1); +} + + + + + + + + + + + + + + +int APS5_VABI sceNpCondSignalAll(PthreadCond* cond) { + return scePthreadCondBroadcast(cond); +} + +int APS5_VABI sceNpCondSignalTo(PthreadCond* cond, Pthread thread) { + return scePthreadCondSignalto(cond, thread); +} + +int APS5_VABI sceNpCondWait(PthreadCond* cond, PthreadMutex* mutex) { + return scePthreadCondWait(cond, mutex); +} + + +int APS5_VABI sceNpLwCondInit(PthreadCond* cond, const char* name) { + return InitCond(cond, name); +} + +int APS5_VABI sceNpLwCondDestroy(PthreadCond* cond) { + return scePthreadCondDestroy(cond); +} + +int APS5_VABI sceNpLwCondSignal(PthreadCond* cond) { + return scePthreadCondSignal(cond); +} + +int APS5_VABI sceNpLwCondSignalAll(PthreadCond* cond) { + return scePthreadCondBroadcast(cond); +} + +int APS5_VABI sceNpLwCondSignalTo(PthreadCond* cond, Pthread thread) { + return scePthreadCondSignalto(cond, thread); +} + +int APS5_VABI sceNpLwCondWait(PthreadCond* cond, PthreadMutex* mutex) { + return scePthreadCondWait(cond, mutex); +} + +int APS5_VABI sceNpCreateEventFlag(KernelEventFlag* flag, const char* name, std::uint32_t attr, std::uint64_t pattern) { + return NpResult(sceKernelCreateEventFlag(flag, name, attr, pattern, nullptr)); +} + +int APS5_VABI sceNpDeleteEventFlag(KernelEventFlag flag) { + return NpResult(sceKernelDeleteEventFlag(flag)); +} + +int APS5_VABI sceNpOpenEventFlag(KernelEventFlag* flag, const char* name) { + return NpResult(sceKernelOpenEventFlag(flag, name)); +} + +int APS5_VABI sceNpSetEventFlag(KernelEventFlag flag, std::uint64_t pattern) { + return NpResult(sceKernelSetEventFlag(flag, pattern)); +} + +int APS5_VABI sceNpClearEventFlag(KernelEventFlag flag, std::uint64_t pattern) { + return NpResult(sceKernelClearEventFlag(flag, pattern)); +} + +int APS5_VABI sceNpPollEventFlag(KernelEventFlag flag, std::uint64_t pattern, std::uint32_t mode, std::uint64_t* result) { + return NpBusyResult(sceKernelPollEventFlag(flag, pattern, mode, result), NP_ERROR_EVENT_FLAG_BUSY); +} + +int APS5_VABI sceNpCancelEventFlag(KernelEventFlag flag, std::uint64_t pattern, int* waiting) { + return NpResult(sceKernelCancelEventFlag(flag, pattern, waiting)); +} + +int APS5_VABI sceNpCreateSema(KernelSema* sema, std::uint32_t flags, int initial, int maximum, const char* name) { + const std::uint32_t attr = KERNEL_SEMA_ATTR_FIFO + ((flags & NP_SEMA_FLAG_PRIORITY) << KERNEL_SEMA_ATTR_PRIORITY_SHIFT); + return NpResult(sceKernelCreateSema(sema, name, attr, initial, maximum, nullptr)); +} + +int APS5_VABI sceNpDeleteSema(KernelSema sema) { + return NpResult(sceKernelDeleteSema(sema)); +} + +int APS5_VABI sceNpPollSema(KernelSema sema) { + return NpBusyResult(sceKernelPollSema(sema, 1), NP_ERROR_SEMA_BUSY); +} + +int APS5_VABI sceNpSignalSema(KernelSema sema) { + return NpResult(sceKernelSignalSema(sema, 1)); +} + +int APS5_VABI sceNpCreateThread(Pthread* thread, PthreadEntry entry, void* arg, int priority, std::size_t stackSize, KernelCpumask affinity, const char* name) { + PthreadAttr attr {}; + int result = scePthreadAttrInit(&attr); + if (result < 0) return result; + result = scePthreadAttrSetstacksize(&attr, stackSize); + if (result < 0) { + scePthreadAttrDestroy(&attr); + return result; + } + if (priority != 0) { + result = scePthreadAttrSetinheritsched(&attr, NP_THREAD_EXPLICIT_SCHED); + if (result >= 0) result = scePthreadAttrSetschedpolicy(&attr, NP_THREAD_SCHED_POLICY); + if (result >= 0) { + KernelSchedParam param {}; + param.sched_priority = priority; + result = scePthreadAttrSetschedparam(&attr, ¶m); + } + } + if (result >= 0 && affinity != 0) result = scePthreadAttrSetaffinity(&attr, affinity); + if (result >= 0) result = scePthreadCreate(thread, &attr, entry, arg, name); + scePthreadAttrDestroy(&attr); + return result; +} + + +int APS5_VABI sceNpThreadGetId(Pthread* thread) { + if (thread == nullptr) return NP_ERROR_INVALID_ARGUMENT; + *thread = scePthreadSelf(); + return 0; +} + +int APS5_VABI sceNpGetProcessId(void) { + return getpid_nid_postfix(); +} + +[[noreturn]] void APS5_VABI sceNpPanic(void) { + std::abort(); +} + +int APS5_VABI sceNpDbgDumpText(const char* text, int length) { + return std::printf("%.*s", length, text); +} + +int APS5_VABI sceNpIpc2ClientInit(void) { + std::lock_guard lock(g_ipcClientMutex); + if (g_ipcClientInitialized) return NP_ERROR_IPC_ALREADY_INITIALIZED; + const int result = InitMemoryHeap(&g_ipcClientHeap, NP_IPC_CLIENT_HEAP_SIZE, NP_IPC_CLIENT_HEAP_NAME); + if (result < 0) return result; + g_ipcClientInitialized = true; + return 0; +} + +int APS5_VABI sceNpIpc2ClientTerm(void) { + std::lock_guard lock(g_ipcClientMutex); + if (!g_ipcClientInitialized) return NP_ERROR_IPC_NOT_INITIALIZED; + DestroyMemoryHeap(&g_ipcClientHeap); + g_ipcClientInitialized = false; + return 0; +} + +int APS5_VABI sceNpArchInit(void) { + if (g_archReferences.fetch_add(1) != 0) return 0; + const int result = InitMemoryHeap(&g_globalHeap, NP_GLOBAL_HEAP_SIZE, NP_GLOBAL_HEAP_NAME); + if (result < 0) g_archReferences.fetch_sub(1); + return result < 0 ? result : 0; +} + +void APS5_VABI sceNpArchTerm(void) { + if (g_archReferences.fetch_sub(1) == 1) DestroyMemoryHeap(&g_globalHeap); +} + +int APS5_VABI sceNpHeapInit(NpHeap* heap, void* base, std::size_t size, const char* name) { + return InitHeap(heap, base, size, name); +} + +void APS5_VABI sceNpHeapDestroy(NpHeap* heap) { + DestroyHeap(heap); +} + +void APS5_VABI sceNpHeapGetAllocator(NpHeap* heap, NpAllocator* allocator) { + *allocator = {HeapAllocate, HeapReallocate, HeapRelease, heap}; +} + +int APS5_VABI sceNpHeapGetStat(const NpHeap* heap, NpHeapStat* stat) { + MallocManagedSize stats {}; + stats.size = sizeof(stats); + stats.version = 1; + const int result = sceLibcMspaceMallocStats_nid_postfix(heap->mspace, &stats); + if (result == 0) *stat = {stats.maxSystemSize, stats.maxInuseSize, stats.currentInuseSize}; + return result; +} + +void APS5_VABI sceNpHeapShowStat(const NpHeap* heap) { + NpHeapStat stat {}; + if (sceNpHeapGetStat(heap, &stat) != 0) { + std::printf("[NpCommon] heap statistics unavailable\n"); + return; + } + std::printf("[NpCommon] heap: system %llu bytes, peak in use %llu bytes, in use %llu bytes\n", + static_cast(stat.maxSystemSize), static_cast(stat.maxInuseSize), + static_cast(stat.currentInuseSize)); +} + +int APS5_VABI sceNpMemoryHeapInit(NpMemoryHeap* heap, std::uint64_t size, const char* name) { + return InitMemoryHeap(heap, size, name); +} + +void APS5_VABI sceNpMemoryHeapDestroy(NpMemoryHeap* heap) { + DestroyMemoryHeap(heap); +} + +void APS5_VABI sceNpMemoryHeapGetAllocator(NpMemoryHeap* heap, NpAllocator* allocator) { + *allocator = MemoryHeapAllocator(heap); +} + +void APS5_VABI sceNpMemoryHeapGetAllocatorEx(NpMemoryHeap* heap, int outOfMemoryError, NpAllocatorEx* allocator) { + *allocator = {MemoryHeapAllocator(heap), outOfMemoryError}; +} + +void APS5_VABI sceNpGlobalHeapGetAllocator(NpAllocator* allocator) { + *allocator = MemoryHeapAllocator(&g_globalHeap); +} + +void APS5_VABI sceNpGlobalHeapGetAllocatorEx(NpAllocatorEx* allocator) { + *allocator = {MemoryHeapAllocator(&g_globalHeap), NP_ERROR_OUT_OF_MEMORY}; +} + +NpAllocator* APS5_VABI sceNpGlobalHeapGetAllocatorPtr(void) { + g_globalAllocator = MemoryHeapAllocator(&g_globalHeap); + return &g_globalAllocator; +} + +NpAllocatorEx* APS5_VABI sceNpGlobalHeapGetAllocatorExPtr(void) { + g_globalAllocatorEx = {MemoryHeapAllocator(&g_globalHeap), NP_ERROR_OUT_OF_MEMORY}; + return &g_globalAllocatorEx; +} + +int APS5_VABI sceNpAllocateKernelMemoryNoAlignment(std::uint64_t size, const char* name, void** address, NpKernelMemory* memory) { + if (memory == nullptr) return NP_ERROR_NULL_OBJECT; + const int result = MapKernelMemory(memory, size, name); + if (result >= 0 && address != nullptr) *address = memory->address; + return result; +} + +int APS5_VABI sceNpAllocateKernelMemoryWithAlignment(std::uint64_t size, const char* name, void** address, NpKernelMemory* memory) { + return sceNpAllocateKernelMemoryNoAlignment(AlignKernelMemory(size), name, address, memory); +} + +int APS5_VABI sceNpFreeKernelMemory(NpKernelMemory* memory) { + if (memory == nullptr) return NP_ERROR_NULL_OBJECT; + UnmapKernelMemory(memory); + return 0; +} + + +void APS5_VABI sceNpGetSdkVersionUInt(std::uint32_t* version) { + const std::uint32_t value = SystemVersion(); + if (version != nullptr) *version = value; +} + + +int APS5_VABI sceNpRtcConvertToPosixTime(const RtcTick* tick, std::int64_t* seconds) { + if (tick->tick == 0) return NP_ERROR_INVALID_ARGUMENT; + RtcDateTime time {}; + const int result = sceRtcSetTick(&time, tick); + if (result < 0) return result; + return NpResult(sceRtcGetTime_t(&time, seconds)); +} + +int APS5_VABI sceNpRtcFormatRFC3339(char* text, std::size_t size, const RtcTick* utc, int timeZoneMinutes) { + if (size < NP_RFC3339_BUFFER_MIN) return NP_ERROR_BUFFER_TOO_SMALL; + const int result = sceRtcFormatRFC3339(text, utc, timeZoneMinutes); + if (result < 0) return result; + text[size - 1] = 0; + return 0; +} + +int APS5_VABI sceNpUserGetUserIdList(int* users, std::size_t capacity, int* count) { + if (capacity < NP_USER_ID_LIST_SIZE) return NP_ERROR_INVALID_ARGUMENT; + int registered[NP_USER_ID_LIST_SIZE]; + std::fill(std::begin(registered), std::end(registered), USER_ID_INVALID); + static_assert(sizeof(UserServiceRegisteredUserIdList) <= sizeof(registered)); + const int result = sceUserServiceGetRegisteredUserIdList(reinterpret_cast(registered)); + if (result < 0) return result; + int found = 0; + for (const int user : registered) + if (user != USER_ID_INVALID) users[found++] = user; + *count = found; + return 0; +} + +int APS5_VABI sceNpRtcParseRFC3339(RtcTick* utc, const char* text) { + return sceRtcParseRFC3339(utc, text); +} + +static std::uint64_t ClockUsec() { + std::int64_t usec = 0; + sceNpGetSystemClockUsec(&usec); + return static_cast(usec); +} + +static void* APS5_VABI CalloutThread(void* arg) { + auto* context = static_cast(arg); + sceNpMutexLock(&context->mutex); + for (;;) { + const std::uint64_t now = ClockUsec(); + NpCallout* head = context->pending; + if (head != nullptr && head->deadline <= now) { + context->pending = head->next; + sceNpMutexUnlock(&context->mutex); + head->handler(head->arg); + sceNpMutexLock(&context->mutex); + continue; + } + if (context->stopping > 0) break; + const std::uint64_t wait = head != nullptr ? std::min(head->deadline - now, NP_CALLOUT_MAX_WAIT) : 0; + sceNpCondTimedwait(&context->cond, &context->mutex, static_cast(wait)); + if (context->stopping > 0) break; + } + sceNpMutexUnlock(&context->mutex); + return nullptr; +} + +int APS5_VABI sceNpCalloutInitCtx(NpCalloutContext* context, const char* name, std::size_t stackSize, int priority, + KernelCpumask affinity) { + if (context->initialized > 0) return NP_ERROR_ALREADY_INITIALIZED; + context->stopping = 0; + context->pending = nullptr; + int result = sceNpMutexInit(&context->mutex, name, 1); + if (result < 0) return result; + result = sceNpCondInit(&context->cond, name, 0); + if (result >= 0) { + result = sceNpCreateThread(&context->thread, CalloutThread, context, priority, stackSize, affinity, name); + if (result >= 0) { + context->initialized = 1; + return 0; + } + sceNpCondDestroy(&context->cond); + } + sceNpMutexDestroy(&context->mutex); + return result; +} + +void APS5_VABI sceNpCalloutTermCtx(NpCalloutContext* context) { + if (context->initialized == 0) return; + sceNpMutexLock(&context->mutex); + context->stopping = 1; + sceNpCondSignal(&context->cond); + sceNpMutexUnlock(&context->mutex); + sceNpJoinThread(context->thread, nullptr); + sceNpCondDestroy(&context->cond); + sceNpMutexDestroy(&context->mutex); + context->initialized = 0; +} + +int APS5_VABI sceNpCalloutStartOnCtx64(NpCalloutContext* context, NpCallout* callout, std::uint64_t usec, + void (APS5_VABI *handler)(void* arg), void* arg) { + if (context->initialized == 0) return NP_ERROR_NOT_INITIALIZED; + *callout = {}; + callout->deadline = ClockUsec() + usec; + callout->handler = handler; + callout->arg = arg; + sceNpMutexLock(&context->mutex); + for (NpCallout* entry = context->pending; entry != nullptr; entry = entry->next) { + if (entry == callout) { + sceNpMutexUnlock(&context->mutex); + return NP_ERROR_ALREADY_QUEUED; + } + } + NpCallout** link = &context->pending; + while (*link != nullptr && callout->deadline > (*link)->deadline) link = &(*link)->next; + callout->next = *link; + *link = callout; + sceNpCondSignal(&context->cond); + sceNpMutexUnlock(&context->mutex); + return 0; +} + +int APS5_VABI sceNpCalloutStartOnCtx(NpCalloutContext* context, NpCallout* callout, std::uint32_t usec, + void (APS5_VABI *handler)(void* arg), void* arg) { + return sceNpCalloutStartOnCtx64(context, callout, usec, handler, arg); +} + +int APS5_VABI sceNpCalloutStopOnCtx(NpCalloutContext* context, NpCallout* callout, int* wasPending) { + if (context->initialized == 0) return NP_ERROR_NOT_INITIALIZED; + sceNpMutexLock(&context->mutex); + int removed = 0; + for (NpCallout** link = &context->pending; *link != nullptr; link = &(*link)->next) { + if (*link != callout) continue; + *link = callout->next; + removed = 1; + break; + } + if (wasPending != nullptr) *wasPending = removed; + sceNpMutexUnlock(&context->mutex); + return 0; +} + +std::uint32_t APS5_VABI sceNpGetRandom(void) { + std::lock_guard lock(g_randomMutex); + return static_cast(g_random()); +} + + +int APS5_VABI sceNpUtilCmpAccountId(std::uint64_t left, std::uint64_t right) { + return left != right ? 1 : 0; +} + +bool APS5_VABI sceNpIntIsOnlineIdString(const char* text) { + return text != nullptr && IsOnlineIdText(text); +} + + +int APS5_VABI sceNpUtilGetNpLanguageCode(int language, char* code) { + const NpLanguageCodes* codes = FindLanguageCodes(language); + if (codes == nullptr || code == nullptr) return NP_ERROR_INVALID_ARGUMENT_UTIL; + CopyText(code, codes->code, NP_LANGUAGE_CODE_SIZE); + return 0; +} + +int APS5_VABI sceNpUtilGetNpLanguageCode2(int language, char* tag) { + const NpLanguageCodes* codes = FindLanguageCodes(language); + if (codes == nullptr || tag == nullptr) return NP_ERROR_INVALID_ARGUMENT_UTIL; + CopyText(tag, codes->tag, NP_LANGUAGE_CODE2_SIZE); + return 0; +} + +const char* APS5_VABI sceNpUtilGetNpLanguageCodeStr(int language) { + const NpLanguageCodes* codes = FindLanguageCodes(language); + return codes != nullptr ? codes->code : nullptr; +} + +const char* APS5_VABI sceNpUtilGetNpLanguageCode2Str(int language) { + const NpLanguageCodes* codes = FindLanguageCodes(language); + return codes != nullptr ? codes->tag : nullptr; +} + +std::uint32_t APS5_VABI sceNpDbgAssignDebugId(void) { + std::uint32_t id = g_debugIds.fetch_add(1) + 1; + if (id == 0) id = g_debugIds.fetch_add(1) + 1; + return id; +} + +int APS5_VABI sceNpUtilBuildTitleId(const NpTitleIdParts* parts, char* titleId, std::size_t size) { + if (parts == nullptr || titleId == nullptr) return NP_ERROR_INVALID_ARGUMENT_UTIL; + if (size < NP_TITLE_ID_SIZE) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (parts->index > NP_TITLE_ID_INDEX_MAX) return NP_UTIL_ERROR_OUT_OF_RANGE; + std::memcpy(titleId, parts->id, sizeof(parts->id)); + titleId[9] = '_'; + titleId[10] = static_cast('0' + parts->index / 10); + titleId[11] = static_cast('0' + parts->index % 10); + titleId[12] = '\0'; + return 0; +} + +void APS5_VABI sceNpDbgDumpBinary(const void* data, std::size_t size) { + const auto* bytes = static_cast(data); + for (std::size_t line = 0; line < size; line += NP_DUMP_LINE_BYTES) { + const std::size_t count = std::min(NP_DUMP_LINE_BYTES, size - line); + for (std::size_t i = 0; i < NP_DUMP_LINE_BYTES; ++i) { + if (i < count) { + std::printf("%02x", bytes[line + i]); + } else { + std::printf(" "); + } + if ((i & 1) != 0) std::putchar(' '); + } + std::putchar(' '); + for (std::size_t i = 0; i < count; ++i) { + const std::uint8_t c = bytes[line + i]; + std::putchar(c >= 0x20 && c <= 0x7E ? c : '.'); + } + std::putchar('\n'); + } + std::putchar('\n'); +} + +} diff --git a/core/libs/prx/libSceNpCommon/Strings.cpp b/core/libs/prx/libSceNpCommon/Strings.cpp new file mode 100644 index 0000000000..d5f70019e9 --- /dev/null +++ b/core/libs/prx/libSceNpCommon/Strings.cpp @@ -0,0 +1,629 @@ +#include +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" + +namespace { + +constexpr int NP_ERROR_INVALID_FORMAT = static_cast(0x80558003); +constexpr int NP_ERROR_INVALID_CHARACTER = static_cast(0x80558004); +constexpr int NP_ERROR_BUFFER_TOO_SMALL = static_cast(0x80558008); +constexpr int NP_ERROR_INVALID_ARGUMENT = static_cast(0x80558009); +constexpr int NP_ERROR_OUT_OF_RANGE = static_cast(0x8055800E); + +constexpr std::uint32_t NP_FORMAT_UPPERCASE = 1; +constexpr std::uint32_t NP_FORMAT_PREFIX = 2; +constexpr std::uint32_t NP_FORMAT_SIGN = 4; +constexpr std::uint32_t NP_RADIX_MAX = 36; + +constexpr char BASE64_ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +constexpr char BASE64URL_ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +constexpr char BASE64_PAD = '='; + +int Base64Value(char c, const char* alphabet) { + if (c == '\0') return -1; + const char* found = std::strchr(alphabet, c); + return found != nullptr ? static_cast(found - alphabet) : -1; +} + +struct Base64Block { + std::uint32_t bits; + int bytes; +}; + +int DecodeLastBlock(const char* src, std::size_t length, const char* alphabet, Base64Block* block) { + char c[4] = {BASE64_PAD, BASE64_PAD, BASE64_PAD, BASE64_PAD}; + std::memcpy(c, src, length); + const int v0 = Base64Value(c[0], alphabet); + const int v1 = Base64Value(c[1], alphabet); + if (v0 < 0 || v1 < 0) return NP_ERROR_INVALID_CHARACTER; + block->bits = (static_cast(v0) << 18) | (static_cast(v1) << 12); + if (c[2] == BASE64_PAD) { + if (c[3] != BASE64_PAD) return NP_ERROR_INVALID_CHARACTER; + block->bytes = 1; + return 0; + } + const int v2 = Base64Value(c[2], alphabet); + if (v2 < 0) return NP_ERROR_INVALID_CHARACTER; + block->bits |= static_cast(v2) << 6; + if (c[3] == BASE64_PAD) { + block->bytes = 2; + return 0; + } + const int v3 = Base64Value(c[3], alphabet); + if (v3 < 0) return NP_ERROR_INVALID_CHARACTER; + block->bits |= static_cast(v3); + block->bytes = 3; + return 0; +} + +int Base64Decode(const char* src, std::size_t length, std::uint8_t* dst, std::size_t capacity, const char* alphabet, + bool padded) { + if (padded && (length & 3) != 0) return NP_ERROR_INVALID_FORMAT; + int decoded = 0; + while (length > 4) { + std::uint32_t bits = 0; + for (int i = 0; i < 4; ++i) { + const int value = Base64Value(src[i], alphabet); + if (value < 0) return NP_ERROR_INVALID_CHARACTER; + bits = (bits << 6) | static_cast(value); + } + if (dst != nullptr) { + if (capacity < 3) return NP_ERROR_INVALID_FORMAT; + capacity -= 3; + *dst++ = static_cast(bits >> 16); + *dst++ = static_cast(bits >> 8); + *dst++ = static_cast(bits); + } + decoded += 3; + src += 4; + length -= 4; + } + if (padded && length != 4) return NP_ERROR_INVALID_FORMAT; + Base64Block block {}; + const int result = DecodeLastBlock(src, length, alphabet, &block); + if (result < 0) return result; + if (dst != nullptr) { + if (capacity < static_cast(block.bytes)) return NP_ERROR_INVALID_FORMAT; + for (int i = 0; i < block.bytes; ++i) dst[i] = static_cast(block.bits >> (16 - 8 * i)); + } + return decoded + block.bytes; +} + +int Base64Encode(const std::uint8_t* src, char* dst, std::size_t length, const char* alphabet, bool padded) { + char* out = dst; + for (; length >= 3; length -= 3, src += 3) { + const std::uint32_t bits = (src[0] << 16) | (src[1] << 8) | src[2]; + for (int shift = 18; shift >= 0; shift -= 6) *out++ = alphabet[(bits >> shift) & 0x3F]; + } + if (length != 0) { + const std::uint32_t bits = (src[0] << 16) | (length == 2 ? src[1] << 8 : 0); + *out++ = alphabet[(bits >> 18) & 0x3F]; + *out++ = alphabet[(bits >> 12) & 0x3F]; + if (length == 2) *out++ = alphabet[(bits >> 6) & 0x3F]; + if (padded) { + if (length == 1) *out++ = BASE64_PAD; + *out++ = BASE64_PAD; + } + } + return static_cast(out - dst); +} + +int HexValue(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +char HexDigit(std::uint32_t value, bool uppercase) { + if (value < 10) return static_cast('0' + value); + return static_cast((uppercase ? 'A' : 'a') + value - 10); +} + +int ParseHex(const char* hex, std::size_t length, std::uint8_t* out, std::size_t capacity) { + if ((length & 1) != 0) return NP_ERROR_INVALID_ARGUMENT; + if (capacity < length / 2) return NP_ERROR_BUFFER_TOO_SMALL; + for (std::size_t i = 0; i < length; i += 2) { + const int high = HexValue(hex[i]); + const int low = HexValue(hex[i + 1]); + if (high < 0 || low < 0) return NP_ERROR_INVALID_ARGUMENT; + out[i / 2] = static_cast((high << 4) | low); + } + return 0; +} + +int FormatInteger(char* buf, std::size_t size, std::uint64_t magnitude, char sign, std::uint32_t radix, + std::uint32_t flags, std::size_t* length) { + if (radix < 1 || radix > NP_RADIX_MAX) return NP_ERROR_INVALID_ARGUMENT; + char prefix[3] = {}; + std::size_t prefixLength = 0; + if (sign != '\0') prefix[prefixLength++] = sign; + if ((flags & NP_FORMAT_PREFIX) != 0 && (radix == 8 || radix == 16)) { + prefix[prefixLength++] = '0'; + if (radix == 16) prefix[prefixLength++] = (flags & NP_FORMAT_UPPERCASE) != 0 ? 'X' : 'x'; + } + std::size_t digits = 1; + if (radix > 1) { + for (std::uint64_t rest = magnitude; rest >= radix; rest /= radix) ++digits; + } + if (buf != nullptr) { + if (size < prefixLength + digits + 1) return NP_ERROR_BUFFER_TOO_SMALL; + std::memcpy(buf, prefix, prefixLength); + char* end = buf + prefixLength + digits; + *end = '\0'; + std::uint64_t rest = magnitude; + for (std::size_t i = 0; i < digits; ++i) { + *--end = HexDigit(static_cast(rest % radix), (flags & NP_FORMAT_UPPERCASE) != 0); + rest /= radix; + } + } + if (length != nullptr) *length = prefixLength + digits; + return 0; +} + +std::uint64_t Magnitude(std::int64_t value) { + return value < 0 ? 0 - static_cast(value) : static_cast(value); +} + +char PlusSign(bool positive, std::uint32_t flags) { + return positive && (flags & NP_FORMAT_SIGN) != 0 ? '+' : '\0'; +} + +template +int ParseInteger(const char* str, std::size_t* parsedLength, Parse parse, bool* saturated) { + char* end = nullptr; + errno = 0; + parse(str, &end); + if (end == str) return NP_ERROR_OUT_OF_RANGE; + if (*saturated && errno == ERANGE) return NP_ERROR_OUT_OF_RANGE; + if (parsedLength != nullptr) *parsedLength = static_cast(end - str); + return 0; +} + +int CopyToBuffer(char* dst, std::size_t size, const char* src, std::size_t length) { + if (size < length + 1) return NP_ERROR_BUFFER_TOO_SMALL; + std::strncpy(dst, src, size - 1); + dst[size - 1] = '\0'; + return 0; +} + +constexpr int NP_UTIL_ERROR_INVALID_ARGUMENT = static_cast(0x80550601); +constexpr int NP_UTIL_ERROR_BUFFER_TOO_SMALL = static_cast(0x80550602); +constexpr int NP_UTIL_ERROR_INVALID_CHARACTER = static_cast(0x80550608); +constexpr std::size_t JSON_ESCAPE_MARGIN = 7; +constexpr std::size_t JSON_UNICODE_ESCAPE_LENGTH = 6; +constexpr std::size_t JSON_SURROGATE_PAIR_LENGTH = 12; +constexpr std::uint32_t SURROGATE_MASK = 0xFC00; +constexpr std::uint32_t HIGH_SURROGATE = 0xD800; +constexpr std::uint32_t LOW_SURROGATE = 0xDC00; +constexpr char URL_HEX_DIGITS[] = "0123456789ABCDEF"; + +const char* JsonShortEscape(unsigned char c) { + switch (c) { + case '\b': return "\\b"; + case '\t': return "\\t"; + case '\n': return "\\n"; + case '\f': return "\\f"; + case '\r': return "\\r"; + case '"': return "\\\""; + case '/': return "\\/"; + case '\\': return "\\\\"; + default: return nullptr; + } +} + +char JsonShortUnescape(char c) { + switch (c) { + case '"': return '"'; + case '\\': return '\\'; + case '/': return '/'; + case 'b': return '\b'; + case 'f': return '\f'; + case 'n': return '\n'; + case 'r': return '\r'; + case 't': return '\t'; + default: return '\0'; + } +} + +int ParseUtf16Unit(const char* text, std::uint32_t* unit) { + *unit = 0; + for (int i = 0; i < 4; ++i) { + const int value = HexValue(text[i]); + if (value < 0) { + *unit = 0; + return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + } + *unit = (*unit << 4) | static_cast(value); + } + return 0; +} + +std::size_t EncodeUtf8(std::uint32_t codePoint, char* out) { + if (codePoint <= 0x7F) { + out[0] = static_cast(codePoint); + return 1; + } + if (codePoint <= 0x7FF) { + out[0] = static_cast(0xC0 | (codePoint >> 6)); + out[1] = static_cast(0x80 | (codePoint & 0x3F)); + return 2; + } + if (codePoint <= 0xFFFF) { + out[0] = static_cast(0xE0 | (codePoint >> 12)); + out[1] = static_cast(0x80 | ((codePoint >> 6) & 0x3F)); + out[2] = static_cast(0x80 | (codePoint & 0x3F)); + return 3; + } + out[0] = static_cast(0xF0 | (codePoint >> 18)); + out[1] = static_cast(0x80 | ((codePoint >> 12) & 0x3F)); + out[2] = static_cast(0x80 | ((codePoint >> 6) & 0x3F)); + out[3] = static_cast(0x80 | (codePoint & 0x3F)); + return 4; +} + +std::uint32_t CombineSurrogates(std::uint32_t high, std::uint32_t low) { + return 0x10000 + (((high & 0x3FF) << 10) | (low & 0x3FF)); +} + +bool IsContinuation(char c) { + return (static_cast(c) & 0xC0) == 0x80; +} + +std::size_t Utf8SequenceLength(unsigned char lead) { + if (lead >= 0xC2 && lead <= 0xDF) return 2; + if ((lead & 0xF0) == 0xE0) return 3; + if ((lead & 0xF8) == 0xF0) return 4; + return 0; +} + +bool IsValidSequence(const char* text, std::size_t length) { + for (std::size_t i = 1; i < length; ++i) { + if (!IsContinuation(text[i])) return false; + } + return true; +} + +bool IsAllowedTextByte(unsigned char c) { + return (c >= 0x20 && c != 0x7F) || c == '\t' || c == '\n' || c == '\r'; +} + +bool IsUrlSafe(unsigned char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' + || c == '*'; +} + +} // namespace + +extern "C" { + +int APS5_VABI sceNpBase64GetDecodeSize(const char* src, std::size_t length) { + return Base64Decode(src, length, nullptr, 0, BASE64_ALPHABET, true); +} + +int APS5_VABI sceNpBase64Decoder(const char* src, std::size_t length, void* dst, std::size_t capacity) { + return Base64Decode(src, length, static_cast(dst), capacity, BASE64_ALPHABET, true); +} + +int APS5_VABI sceNpBase64Encoder(const void* src, char* dst, std::size_t length) { + return Base64Encode(static_cast(src), dst, length, BASE64_ALPHABET, true); +} + +int APS5_VABI sceNpBase64UrlGetDecodeSize(const char* src, std::size_t length) { + return Base64Decode(src, length, nullptr, 0, BASE64URL_ALPHABET, false); +} + +int APS5_VABI sceNpBase64UrlDecoder(const char* src, std::size_t length, void* dst, std::size_t capacity) { + return Base64Decode(src, length, static_cast(dst), capacity, BASE64URL_ALPHABET, false); +} + +int APS5_VABI sceNpBase64UrlEncoder(const void* src, char* dst, std::size_t length) { + return Base64Encode(static_cast(src), dst, length, BASE64URL_ALPHABET, false); +} + +int APS5_VABI sceNpHexToInt(int c) { + return HexValue(static_cast(c)); +} + +int APS5_VABI sceNpIntToHex(std::uint32_t value, bool uppercase) { + return HexDigit(value, uppercase); +} + +int APS5_VABI sceNpUInt32ToStr(char* buf, std::size_t size, std::uint32_t value, std::uint32_t radix, std::uint32_t flags, + std::size_t* length) { + return FormatInteger(buf, size, value, PlusSign(value != 0, flags), radix, flags, length); +} + +int APS5_VABI sceNpUInt64ToStr(char* buf, std::size_t size, std::uint64_t value, std::uint32_t radix, std::uint32_t flags, + std::size_t* length) { + return FormatInteger(buf, size, value, PlusSign(value != 0, flags), radix, flags, length); +} + +int APS5_VABI sceNpInt32ToStr(char* buf, std::size_t size, std::int32_t value, std::uint32_t radix, std::uint32_t flags, + std::size_t* length) { + const char sign = value < 0 ? '-' : PlusSign(value > 0, flags); + return FormatInteger(buf, size, Magnitude(value), sign, radix, flags, length); +} + +int APS5_VABI sceNpInt64ToStr(char* buf, std::size_t size, std::int64_t value, std::uint32_t radix, std::uint32_t flags, + std::size_t* length) { + const char sign = (flags & NP_FORMAT_SIGN) == 0 ? '\0' : value < 0 ? '-' : PlusSign(value > 0, flags); + return FormatInteger(buf, size, Magnitude(value), sign, radix, flags, length); +} + +int APS5_VABI sceNpStrToInt32(const char* str, std::int32_t* value, int base, std::size_t* parsedLength) { + long parsed = 0; + bool saturated = false; + const int result = ParseInteger(str, parsedLength, [&](const char* s, char** end) { + parsed = std::strtol(s, end, base); + saturated = parsed == LONG_MIN || parsed == LONG_MAX; + }, &saturated); + if (result < 0) return result; + if (parsed != static_cast(parsed)) return NP_ERROR_OUT_OF_RANGE; + *value = static_cast(parsed); + return 0; +} + +int APS5_VABI sceNpStrToInt64(const char* str, std::int64_t* value, int base, std::size_t* parsedLength) { + long long parsed = 0; + bool saturated = false; + const int result = ParseInteger(str, parsedLength, [&](const char* s, char** end) { + parsed = std::strtoll(s, end, base); + saturated = parsed == LLONG_MIN || parsed == LLONG_MAX; + }, &saturated); + if (result < 0) return result; + *value = parsed; + return 0; +} + +int APS5_VABI sceNpStrToUInt32(const char* str, std::uint32_t* value, int base, std::size_t* parsedLength) { + unsigned long long parsed = 0; + bool saturated = false; + const int result = ParseInteger(str, parsedLength, [&](const char* s, char** end) { + parsed = std::strtoull(s, end, base); + saturated = parsed == ULLONG_MAX; + }, &saturated); + if (result < 0) return result; + if ((parsed >> 32) != 0) return NP_ERROR_OUT_OF_RANGE; + *value = static_cast(parsed); + return 0; +} + +int APS5_VABI sceNpStrToUInt64(const char* str, std::uint64_t* value, int base, std::size_t* parsedLength) { + unsigned long long parsed = 0; + bool saturated = false; + const int result = ParseInteger(str, parsedLength, [&](const char* s, char** end) { + parsed = std::strtoull(s, end, base); + saturated = parsed == ULLONG_MAX; + }, &saturated); + if (result < 0) return result; + *value = parsed; + return 0; +} + +int APS5_VABI sceNpStrBuildHex(char* dst, std::size_t size, const void* src, std::size_t length, bool uppercase) { + if (size <= length * 2) return NP_ERROR_BUFFER_TOO_SMALL; + const auto* bytes = static_cast(src); + for (std::size_t i = 0; i < length; ++i) { + dst[i * 2] = HexDigit(bytes[i] >> 4, uppercase); + dst[i * 2 + 1] = HexDigit(bytes[i] & 0xF, uppercase); + } + dst[length * 2] = '\0'; + return 0; +} + +int APS5_VABI sceNpStrParseHex(const char* hex, void* out, std::size_t capacity) { + return ParseHex(hex, std::strlen(hex), static_cast(out), capacity); +} + +int APS5_VABI sceNpStrnParseHex(const char* hex, std::size_t length, void* out, std::size_t capacity) { + return ParseHex(hex, length, static_cast(out), capacity); +} + +int APS5_VABI sceNpStrcpyToBuf(char* dst, std::size_t size, const char* src) { + return CopyToBuffer(dst, size, src, std::strlen(src)); +} + +int APS5_VABI sceNpStrncpyToBuf(char* dst, std::size_t size, const char* src, std::size_t maxLength) { + return CopyToBuffer(dst, size, src, strnlen(src, maxLength)); +} + +int APS5_VABI sceNpUtilJsonEscape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength) { + if (dst == nullptr || dstSize == 0 || src == nullptr || srcLength == 0) return NP_UTIL_ERROR_INVALID_ARGUMENT; + if (dstSize < JSON_ESCAPE_MARGIN) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + std::size_t written = 0; + for (std::size_t i = 0; i < srcLength; ++i) { + if (written > dstSize - JSON_ESCAPE_MARGIN) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + const auto c = static_cast(src[i]); + if (c == 0) return 0; + char piece[JSON_ESCAPE_MARGIN] = {}; + if (const char* escape = JsonShortEscape(c)) { + std::strcpy(piece, escape); + } else if (c >= 0x20 && c != 0x7F) { + piece[0] = static_cast(c); + } else { + std::snprintf(piece, sizeof(piece), "\\u%04x", c); + } + std::strcat(dst, piece); + written += std::strlen(piece); + } + return 0; +} + +int APS5_VABI sceNpUtilJsonUnescape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, + std::size_t* dstLength) { + if (src == nullptr || srcLength == 0) return NP_UTIL_ERROR_INVALID_ARGUMENT; + if (dst == nullptr && dstLength == nullptr) return NP_UTIL_ERROR_INVALID_ARGUMENT; + const char* end = src + srcLength; + std::size_t count = 0; + int status = 0; + char* out = dst; + auto emit = [&](const char* bytes, std::size_t length, std::size_t consumed) { + if (out != nullptr) { + std::memcpy(out, bytes, length); + out += length; + } + count += length; + src += consumed; + }; + while (src < end) { + if (dst != nullptr && count + 1 > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + const std::size_t remaining = static_cast(end - src); + if (src[0] != '\\' || remaining < 2) { + emit(src, 1, 1); + continue; + } + if (const char simple = JsonShortUnescape(src[1])) { + emit(&simple, 1, 2); + continue; + } + if (src[1] != 'u' || remaining < JSON_UNICODE_ESCAPE_LENGTH) { + emit(src, 1, 1); + continue; + } + std::uint32_t unit = 0; + int result = ParseUtf16Unit(src + 2, &unit); + if (result < 0) { + status = result; + emit(src, 1, 1); + continue; + } + status = 0; + char encoded[4] = {}; + if ((unit & SURROGATE_MASK) != HIGH_SURROGATE) { + const std::size_t length = EncodeUtf8(unit, encoded); + if (dst != nullptr && count + length > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + emit(encoded, length, JSON_UNICODE_ESCAPE_LENGTH); + continue; + } + std::uint32_t low = 0; + if (remaining < JSON_SURROGATE_PAIR_LENGTH || src[6] != '\\' || src[7] != 'u') { + emit(src, 1, 1); + continue; + } + result = ParseUtf16Unit(src + 8, &low); + if (result < 0) { + status = result; + emit(src, 1, 1); + continue; + } + if ((low & SURROGATE_MASK) != LOW_SURROGATE) { + emit(src, 1, 1); + continue; + } + const std::size_t length = EncodeUtf8(CombineSurrogates(unit, low), encoded); + if (dst != nullptr && count + length > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + emit(encoded, length, JSON_SURROGATE_PAIR_LENGTH); + } + ++count; + if (count > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (dstLength != nullptr) *dstLength = count; + if (out != nullptr) *out = '\0'; + return status; +} + +int APS5_VABI sceNpUtilJsonGetOneChar(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, + std::size_t* dstLength, std::size_t* consumed) { + if (src == nullptr || srcLength == 0) return NP_UTIL_ERROR_INVALID_ARGUMENT; + if (dst == nullptr && dstLength == nullptr) return NP_UTIL_ERROR_INVALID_ARGUMENT; + std::size_t decoded = 1; + std::size_t used = 1; + int status = 0; + char encoded[4] = {}; + const auto lead = static_cast(src[0]); + if (lead == '\\') { + used = 2; + if (srcLength < 2) { + status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; + } else if (const char simple = JsonShortUnescape(src[1])) { + encoded[0] = simple; + } else if (src[1] != 'u') { + used = 1; + encoded[0] = '\\'; + } else { + used = JSON_UNICODE_ESCAPE_LENGTH; + std::uint32_t unit = 0; + if (srcLength < JSON_UNICODE_ESCAPE_LENGTH || ParseUtf16Unit(src + 2, &unit) < 0) { + status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; + } else if ((unit & SURROGATE_MASK) != HIGH_SURROGATE) { + decoded = EncodeUtf8(unit, encoded); + } else { + used = JSON_SURROGATE_PAIR_LENGTH; + std::uint32_t low = 0; + if (srcLength < JSON_SURROGATE_PAIR_LENGTH || ParseUtf16Unit(src + 8, &low) < 0 + || (low & SURROGATE_MASK) != LOW_SURROGATE) { + status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; + } else { + decoded = EncodeUtf8(CombineSurrogates(unit, low), encoded); + } + } + } + } else if (const std::size_t length = Utf8SequenceLength(lead); length != 0 && srcLength < length) { + decoded = 0; + used = length; + status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; + } else if (length != 0 && IsValidSequence(src, length)) { + decoded = length; + used = length; + std::memcpy(encoded, src, length); + } else { + encoded[0] = src[0]; + } + if (status == 0 && dst != nullptr && dstSize < decoded) status = NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (status == 0 && dst != nullptr) std::memcpy(dst, encoded, decoded); + if (dstLength != nullptr) *dstLength = decoded; + if (consumed != nullptr) *consumed = used; + return status; +} + +int APS5_VABI sceNpUtilGetNthChar(std::size_t index, const char* text, const char** character) { + if (text == nullptr) return NP_UTIL_ERROR_INVALID_ARGUMENT; + const char* position = text; + for (std::size_t i = 0; i < index && *position != '\0'; ++i) { + const auto lead = static_cast(*position); + std::size_t length = 1; + if (lead < 0x80) { + if (!IsAllowedTextByte(lead)) return NP_UTIL_ERROR_INVALID_CHARACTER; + } else { + length = Utf8SequenceLength(lead); + if (length == 0 || !IsValidSequence(position, length)) return NP_UTIL_ERROR_INVALID_CHARACTER; + } + position += length; + } + *character = position; + return 0; +} + +int APS5_VABI sceNpUtilHttpUrlEncode(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, + std::size_t* dstLength) { + if (src == nullptr || srcLength == 0) return NP_UTIL_ERROR_INVALID_ARGUMENT; + if (dst == nullptr && dstLength == nullptr) return NP_UTIL_ERROR_INVALID_ARGUMENT; + std::size_t count = 0; + for (std::size_t i = 0; i < srcLength; ++i) { + const auto c = static_cast(src[i]); + const bool escaped = c != ' ' && !IsUrlSafe(c); + const std::size_t length = escaped ? 3 : 1; + if (dst != nullptr && count + length > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (dst != nullptr) { + if (escaped) { + dst[count] = '%'; + dst[count + 1] = URL_HEX_DIGITS[c >> 4]; + dst[count + 2] = URL_HEX_DIGITS[c & 0xF]; + } else { + dst[count] = c == ' ' ? '+' : static_cast(c); + } + } + count += length; + } + if (dst != nullptr && count + 1 > dstSize) return NP_UTIL_ERROR_BUFFER_TOO_SMALL; + if (dst != nullptr) dst[count] = '\0'; + if (dstLength != nullptr) *dstLength = count + 1; + return 0; +} + +} diff --git a/core/libs/tests/GuestNpCommonRuntime.cpp b/core/libs/tests/GuestNpCommonRuntime.cpp new file mode 100644 index 0000000000..e019a7b313 --- /dev/null +++ b/core/libs/tests/GuestNpCommonRuntime.cpp @@ -0,0 +1,495 @@ +#include "SceTypes.hpp" + +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpAtomicCas32(std::int32_t* value, std::int32_t expected, std::int32_t desired); +int APS5_VABI sceNpAtomicInc32(std::int32_t* value); +int APS5_VABI sceNpAtomicDec32(std::int32_t* value); +int APS5_VABI sceNpCreateSema(KernelSema* sema, std::uint32_t flags, int initial, int maximum, const char* name); +int APS5_VABI sceNpDeleteSema(KernelSema sema); +int APS5_VABI sceNpPollSema(KernelSema sema); +int APS5_VABI sceNpSignalSema(KernelSema sema); +int APS5_VABI sceNpCreateEventFlag(KernelEventFlag* flag, const char* name, std::uint32_t attr, std::uint64_t pattern); +int APS5_VABI sceNpDeleteEventFlag(KernelEventFlag flag); +int APS5_VABI sceNpSetEventFlag(KernelEventFlag flag, std::uint64_t pattern); +int APS5_VABI sceNpPollEventFlag(KernelEventFlag flag, std::uint64_t pattern, std::uint32_t mode, std::uint64_t* result); +int APS5_VABI sceNpCreateThread(Pthread* thread, PthreadEntry entry, void* arg, int priority, std::size_t stackSize, KernelCpumask affinity, const char* name); +int APS5_VABI sceNpJoinThread(Pthread thread, void** value); +int APS5_VABI sceNpThreadGetId(Pthread* thread); + +struct NpHeap { + void* mspace; +}; + +struct NpHeapStat { + std::uint64_t maxSystemSize; + std::uint64_t maxInuseSize; + std::uint64_t currentInuseSize; +}; + +struct NpKernelMemory { + std::uint64_t size; + std::uint64_t reserved; + void* address; +}; + +struct NpMemoryHeap { + NpKernelMemory memory; + NpHeap heap; +}; + +struct NpAllocator { + void* (APS5_VABI *allocate)(std::size_t size, void* context); + void* (APS5_VABI *reallocate)(void* pointer, std::size_t size, void* context); + void (APS5_VABI *release)(void* pointer, void* context); + void* context; +}; + +struct NpAllocatorEx { + NpAllocator allocator; + int outOfMemoryError; +}; + +int APS5_VABI sceNpHeapInit(NpHeap* heap, void* base, std::size_t size, const char* name); +void APS5_VABI sceNpHeapDestroy(NpHeap* heap); +void APS5_VABI sceNpHeapGetAllocator(NpHeap* heap, NpAllocator* allocator); +int APS5_VABI sceNpHeapGetStat(const NpHeap* heap, NpHeapStat* stat); +int APS5_VABI sceNpMemoryHeapInit(NpMemoryHeap* heap, std::uint64_t size, const char* name); +void APS5_VABI sceNpMemoryHeapDestroy(NpMemoryHeap* heap); +void APS5_VABI sceNpMemoryHeapGetAllocatorEx(NpMemoryHeap* heap, int outOfMemoryError, NpAllocatorEx* allocator); +int APS5_VABI sceNpArchInit(void); +int APS5_VABI sceNpIpc2ClientInit(void); +int APS5_VABI sceNpIpc2ClientTerm(void); +void APS5_VABI sceNpArchTerm(void); +NpAllocatorEx* APS5_VABI sceNpGlobalHeapGetAllocatorExPtr(void); +int APS5_VABI sceNpAllocateKernelMemoryWithAlignment(std::uint64_t size, const char* name, void** address, NpKernelMemory* memory); +int APS5_VABI sceNpFreeKernelMemory(NpKernelMemory* memory); +struct NpCallout { + NpCallout* next; + void (APS5_VABI *handler)(void* arg); + std::uint64_t deadline; + void* arg; + std::uint64_t reserved; +}; + +struct NpCalloutContext { + Pthread thread; + PthreadMutex mutex; + PthreadCond cond; + int initialized; + int stopping; + NpCallout* pending; +}; + +int APS5_VABI sceNpCalloutInitCtx(NpCalloutContext* context, const char* name, std::size_t stackSize, int priority, KernelCpumask affinity); +void APS5_VABI sceNpCalloutTermCtx(NpCalloutContext* context); +int APS5_VABI sceNpCalloutStartOnCtx(NpCalloutContext* context, NpCallout* callout, std::uint32_t usec, void (APS5_VABI *handler)(void* arg), void* arg); +int APS5_VABI sceNpCalloutStopOnCtx(NpCalloutContext* context, NpCallout* callout, int* wasPending); +int APS5_VABI sceNpUtilCmpAccountId(std::uint64_t left, std::uint64_t right); +bool APS5_VABI sceNpIntIsOnlineIdString(const char* text); +int APS5_VABI sceNpUtilGetNpLanguageCode(int language, char* code); +int APS5_VABI sceNpUtilGetNpLanguageCode2(int language, char* tag); +const char* APS5_VABI sceNpUtilGetNpLanguageCodeStr(int language); +const char* APS5_VABI sceNpUtilGetNpLanguageCode2Str(int language); +std::uint32_t APS5_VABI sceNpDbgAssignDebugId(void); +int APS5_VABI sceNpUtilJsonEscape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength); +int APS5_VABI sceNpUtilJsonUnescape(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* dstLength); +int APS5_VABI sceNpUtilJsonGetOneChar(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* dstLength, std::size_t* consumed); +int APS5_VABI sceNpUtilGetNthChar(std::size_t index, const char* text, const char** character); +int APS5_VABI sceNpUtilHttpUrlEncode(char* dst, std::size_t dstSize, const char* src, std::size_t srcLength, std::size_t* dstLength); +struct NpTitleIdParts { + char id[9]; + char reserved; + std::uint8_t index; +}; + +int APS5_VABI sceNpUtilBuildTitleId(const NpTitleIdParts* parts, char* titleId, std::size_t size); +int APS5_VABI sceNpBase64GetDecodeSize(const char* src, std::size_t length); +int APS5_VABI sceNpBase64Decoder(const char* src, std::size_t length, void* dst, std::size_t capacity); +int APS5_VABI sceNpBase64Encoder(const void* src, char* dst, std::size_t length); +int APS5_VABI sceNpBase64UrlGetDecodeSize(const char* src, std::size_t length); +int APS5_VABI sceNpBase64UrlDecoder(const char* src, std::size_t length, void* dst, std::size_t capacity); +int APS5_VABI sceNpBase64UrlEncoder(const void* src, char* dst, std::size_t length); +int APS5_VABI sceNpHexToInt(int c); +int APS5_VABI sceNpIntToHex(std::uint32_t value, bool uppercase); +int APS5_VABI sceNpUInt32ToStr(char* buf, std::size_t size, std::uint32_t value, std::uint32_t radix, std::uint32_t flags, std::size_t* length); +int APS5_VABI sceNpUInt64ToStr(char* buf, std::size_t size, std::uint64_t value, std::uint32_t radix, std::uint32_t flags, std::size_t* length); +int APS5_VABI sceNpInt32ToStr(char* buf, std::size_t size, std::int32_t value, std::uint32_t radix, std::uint32_t flags, std::size_t* length); +int APS5_VABI sceNpInt64ToStr(char* buf, std::size_t size, std::int64_t value, std::uint32_t radix, std::uint32_t flags, std::size_t* length); +int APS5_VABI sceNpStrToInt32(const char* str, std::int32_t* value, int base, std::size_t* parsedLength); +int APS5_VABI sceNpStrToInt64(const char* str, std::int64_t* value, int base, std::size_t* parsedLength); +int APS5_VABI sceNpStrToUInt32(const char* str, std::uint32_t* value, int base, std::size_t* parsedLength); +int APS5_VABI sceNpStrToUInt64(const char* str, std::uint64_t* value, int base, std::size_t* parsedLength); +int APS5_VABI sceNpStrBuildHex(char* dst, std::size_t size, const void* src, std::size_t length, bool uppercase); +int APS5_VABI sceNpStrParseHex(const char* hex, void* out, std::size_t capacity); +int APS5_VABI sceNpStrnParseHex(const char* hex, std::size_t length, void* out, std::size_t capacity); +int APS5_VABI sceNpStrcpyToBuf(char* dst, std::size_t size, const char* src); +int APS5_VABI sceNpStrncpyToBuf(char* dst, std::size_t size, const char* src, std::size_t maxLength); +} + +namespace { + +constexpr int NpIpcAlreadyInitialized = static_cast(0x80559D00); +constexpr int NpIpcNotInitialized = static_cast(0x80559D01); +constexpr int NpOutOfMemory = static_cast(0x80550005); +constexpr int NpNullObject = static_cast(0x80558005); +constexpr int NpNotInitialized = static_cast(0x80558001); +constexpr int NpAlreadyInitialized = static_cast(0x80558002); +constexpr int NpAlreadyQueued = static_cast(0x80558006); +constexpr int NpUtilInvalid = static_cast(0x80550601); +constexpr int NpUtilBufferTooSmall = static_cast(0x80550602); +constexpr int NpUtilOutOfRange = static_cast(0x8055060A); +constexpr int NpUtilInvalidCharacter = static_cast(0x80550608); +constexpr int NpInvalidFormat = static_cast(0x80558003); +constexpr int NpInvalidCharacter = static_cast(0x80558004); +constexpr int NpBufferTooSmall = static_cast(0x80558008); +constexpr int NpOutOfRange = static_cast(0x8055800E); +constexpr int NpInvalidArgument = static_cast(0x80558009); +constexpr int NpEventFlagBusy = static_cast(0x8055800C); +constexpr int NpSemaBusy = static_cast(0x8055800D); +constexpr std::uint32_t EventFlagWaitAnd = 1; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +void* APS5_VABI ReturnArgument(void* arg) { + Pthread self {}; + Require(sceNpThreadGetId(&self) == 0 && self != nullptr, "a created thread can read its id"); + return arg; +} + +void TestAtomics() { + std::int32_t value = 5; + Require(sceNpAtomicInc32(&value) == 5 && value == 6, "increment returns the previous value"); + Require(sceNpAtomicDec32(&value) == 6 && value == 5, "decrement returns the previous value"); + Require(sceNpAtomicCas32(&value, 4, 9) == 5 && value == 5, "a failed compare-and-swap returns the current value"); + Require(sceNpAtomicCas32(&value, 5, 9) == 5 && value == 9, "a successful compare-and-swap returns the previous value"); +} + +void TestSemaphoresAndFlags() { + KernelSema sema {}; + Require(sceNpCreateSema(&sema, 0, 0, 2, "NpSema") == 0, "a semaphore is created"); + Require(sceNpPollSema(sema) == NpSemaBusy, "an empty semaphore reports the busy error"); + Require(sceNpSignalSema(sema) == 0 && sceNpPollSema(sema) == 0, "a signalled semaphore can be taken once"); + Require(sceNpDeleteSema(sema) == 0, "the semaphore is deleted"); + + KernelEventFlag flag {}; + Require(sceNpCreateEventFlag(&flag, "NpFlag", 0, 0) == 0, "an event flag is created"); + std::uint64_t result = 0; + Require(sceNpPollEventFlag(flag, 1, EventFlagWaitAnd, &result) == NpEventFlagBusy, "a clear event flag reports the busy error"); + Require(sceNpSetEventFlag(flag, 1) == 0 && sceNpPollEventFlag(flag, 1, EventFlagWaitAnd, &result) == 0 && result == 1, "a set bit is polled"); + Require(sceNpDeleteEventFlag(flag) == 0, "the event flag is deleted"); +} + +void TestThreads() { + Require(sceNpThreadGetId(nullptr) == NpInvalidArgument, "a null thread id output is refused"); + Pthread thread {}; + int marker = 0; + Require(sceNpCreateThread(&thread, ReturnArgument, &marker, 0, 0x10000, 0, "NpThread") == 0, "a thread is created"); + void* value = nullptr; + Require(sceNpJoinThread(thread, &value) == 0 && value == &marker, "the thread is joined with its return value"); +} + +void UseAllocator(const NpAllocator& allocator) { + auto* block = static_cast(allocator.allocate(64, allocator.context)); + Require(block != nullptr, "the allocator hands out memory"); + block[63] = 0x5A; + block = static_cast(allocator.reallocate(block, 256, allocator.context)); + Require(block != nullptr && block[63] == 0x5A, "reallocation keeps the contents"); + allocator.release(block, allocator.context); + allocator.release(nullptr, allocator.context); +} + +void TestHeaps() { + NpMemoryHeap memoryHeap {}; + Require(sceNpMemoryHeapInit(nullptr, 0x1000, "np-test") == NpNullObject, "a null memory heap is refused"); + Require(sceNpMemoryHeapInit(&memoryHeap, 0x1001, "np-test") == 0, "a memory heap is created"); + Require(memoryHeap.memory.size == 0x4000 && memoryHeap.memory.address != nullptr && memoryHeap.heap.mspace != nullptr, + "the memory heap size is rounded up to 16 KiB"); + NpAllocatorEx allocatorEx {}; + sceNpMemoryHeapGetAllocatorEx(&memoryHeap, 0x1234, &allocatorEx); + Require(allocatorEx.allocator.context == &memoryHeap && allocatorEx.outOfMemoryError == 0x1234, + "the extended allocator carries the caller's error code"); + UseAllocator(allocatorEx.allocator); + sceNpMemoryHeapDestroy(&memoryHeap); + Require(memoryHeap.memory.address == nullptr && memoryHeap.heap.mspace == nullptr, "destroying clears the memory heap"); + + NpKernelMemory memory {}; + void* address = nullptr; + Require(sceNpAllocateKernelMemoryWithAlignment(0x10, "np-test", &address, nullptr) == NpNullObject, + "kernel memory needs an object"); + Require(sceNpAllocateKernelMemoryWithAlignment(0x10, "np-test", &address, &memory) == 0 + && address == memory.address && memory.size == 0x4000, + "kernel memory is mapped in 16 KiB units"); + NpHeap heap {}; + Require(sceNpHeapInit(&heap, memory.address, memory.size, "np-test") == 0 && heap.mspace != nullptr, "a heap is created"); + NpAllocator allocator {}; + sceNpHeapGetAllocator(&heap, &allocator); + Require(allocator.context == &heap, "the heap allocator uses the heap as its context"); + UseAllocator(allocator); + NpHeapStat stat {}; + Require(sceNpHeapGetStat(&heap, &stat) == 0 && stat.maxInuseSize >= stat.currentInuseSize, "heap statistics are reported"); + sceNpHeapDestroy(&heap); + Require(heap.mspace == nullptr, "destroying clears the heap"); + Require(sceNpFreeKernelMemory(&memory) == 0 && memory.address == nullptr, "kernel memory is released"); + Require(sceNpFreeKernelMemory(nullptr) == NpNullObject, "freeing needs an object"); + + Require(sceNpArchInit() == 0 && sceNpArchInit() == 0, "the library initialises more than once"); + NpAllocatorEx* global = sceNpGlobalHeapGetAllocatorExPtr(); + Require(global->outOfMemoryError == NpOutOfMemory, "the global allocator reports the library's out-of-memory error"); + UseAllocator(global->allocator); + sceNpArchTerm(); + UseAllocator(sceNpGlobalHeapGetAllocatorExPtr()->allocator); + sceNpArchTerm(); + + Require(sceNpIpc2ClientTerm() == NpIpcNotInitialized, "the ipc client must be started before it stops"); + Require(sceNpIpc2ClientInit() == 0 && sceNpIpc2ClientInit() == NpIpcAlreadyInitialized, "the ipc client starts once"); + Require(sceNpIpc2ClientTerm() == 0 && sceNpIpc2ClientInit() == 0 && sceNpIpc2ClientTerm() == 0, "the ipc client restarts"); +} + +struct CalloutRecord { + std::atomic order {0}; + std::atomic first {0}; + std::atomic second {0}; +}; + +CalloutRecord g_callouts; + +void APS5_VABI RecordFirst(void*) { + g_callouts.first = ++g_callouts.order; +} + +void APS5_VABI RecordSecond(void*) { + g_callouts.second = ++g_callouts.order; +} + +void APS5_VABI RecordUnexpected(void*) { + Require(false, "a stopped callout does not run"); +} + +void TestCallouts() { + NpCalloutContext context {}; + NpCallout callout {}; + Require(sceNpCalloutStartOnCtx(&context, &callout, 0, RecordFirst, nullptr) == NpNotInitialized, "a context must be initialised first"); + Require(sceNpCalloutInitCtx(&context, "np-callout", 0x4000, 0, 0) == 0, "a callout context starts"); + Require(sceNpCalloutInitCtx(&context, "np-callout", 0x4000, 0, 0) == NpAlreadyInitialized, "a context starts once"); + + NpCallout stopped {}; + int wasPending = 0; + Require(sceNpCalloutStartOnCtx(&context, &stopped, 60000000, RecordUnexpected, nullptr) == 0, "a distant callout is queued"); + Require(sceNpCalloutStartOnCtx(&context, &stopped, 60000000, RecordUnexpected, nullptr) == NpAlreadyQueued, + "a queued callout cannot be queued again"); + Require(sceNpCalloutStopOnCtx(&context, &stopped, &wasPending) == 0 && wasPending == 1, "a queued callout is removed"); + Require(sceNpCalloutStopOnCtx(&context, &stopped, &wasPending) == 0 && wasPending == 0, "removing twice reports nothing pending"); + + NpCallout later {}; + Require(sceNpCalloutStartOnCtx(&context, &later, 20000, RecordSecond, nullptr) == 0, "a later callout is queued"); + Require(sceNpCalloutStartOnCtx(&context, &callout, 1000, RecordFirst, nullptr) == 0, "an earlier callout is queued"); + for (int i = 0; i < 2000 && g_callouts.second == 0; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(g_callouts.first == 1 && g_callouts.second == 2, "callouts run in deadline order"); + sceNpCalloutTermCtx(&context); + Require(context.initialized == 0, "terminating clears the context"); + sceNpCalloutTermCtx(&context); +} + +bool Equals(const char* actual, const char* expected) { + return std::strcmp(actual, expected) == 0; +} + +void TestIds() { + Require(sceNpUtilCmpAccountId(7, 7) == 0 && sceNpUtilCmpAccountId(7, 8) == 1, "account ids compare for equality"); + + Require(sceNpIntIsOnlineIdString("Alice_01") && sceNpIntIsOnlineIdString("Bob-2"), "letters, digits, '_' and '-' are allowed"); + Require(!sceNpIntIsOnlineIdString("ab") && !sceNpIntIsOnlineIdString("abcdefghijklmnopq"), "online ids are 3 to 16 characters"); + Require(!sceNpIntIsOnlineIdString("1abc") && !sceNpIntIsOnlineIdString("ab.c") && !sceNpIntIsOnlineIdString(nullptr), + "online ids start with a letter and use no other symbols"); +} + +void TestJsonText() { + char escaped[64] = {}; + Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), "a\"b/\n\x01", 7) == 0 && Equals(escaped, "a\\\"b\\/\\n\\u0001"), + "json escaping covers quotes, solidus, newlines and controls"); + std::strcpy(escaped, "x"); + Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), "yz", 2) == 0 && Equals(escaped, "xyz"), "escaping appends to the buffer"); + Require(sceNpUtilJsonEscape(escaped, 6, "a", 1) == NpUtilBufferTooSmall, "escaping needs room for one escape"); + Require(sceNpUtilJsonEscape(escaped, sizeof(escaped), nullptr, 1) == NpUtilInvalid, "escaping needs a source"); + + char text[32] = {}; + std::size_t length = 0; + Require(sceNpUtilJsonUnescape(text, sizeof(text), "a\\nb\\u0041", 10, &length) == 0 && Equals(text, "a\nbA") && length == 5, + "json unescaping decodes escapes and counts the terminator"); + Require(sceNpUtilJsonUnescape(text, sizeof(text), "\\uD83D\\uDE00", 12, &length) == 0 && Equals(text, "\xF0\x9F\x98\x80") + && length == 5, + "surrogate pairs become one UTF-8 character"); + Require(sceNpUtilJsonUnescape(text, sizeof(text), "\\u00G1", 6, &length) == NpUtilBufferTooSmall && Equals(text, "\\u00G1"), + "a bad unicode escape is copied and reported"); + Require(sceNpUtilJsonUnescape(nullptr, sizeof(text), "abc", 3, &length) == 0 && length == 4, "a null buffer reports the size"); + Require(sceNpUtilJsonUnescape(text, 3, "abc", 3, &length) == NpUtilBufferTooSmall, "unescaping needs room for the terminator"); + + char one[4] = {}; + std::size_t consumed = 0; + Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\\u00e9x", 7, &length, &consumed) == 0 && length == 2 && consumed == 6 + && std::memcmp(one, "\xC3\xA9", 2) == 0, + "one escaped character is decoded"); + Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\xE6\x97\xA5", 3, &length, &consumed) == 0 && length == 3 && consumed == 3, + "one UTF-8 character is copied"); + Require(sceNpUtilJsonGetOneChar(one, sizeof(one), "\xE6\x97", 2, &length, &consumed) == NpUtilBufferTooSmall && length == 0 + && consumed == 3, + "a cut UTF-8 character reports its full length"); + Require(sceNpUtilJsonGetOneChar(one, 1, "\xC3\xA9", 2, &length, &consumed) == NpUtilBufferTooSmall && length == 2, + "a short buffer still reports the decoded size"); + + const char* character = nullptr; + const char* japanese = "\xE6\x97\xA5" "a"; + Require(sceNpUtilGetNthChar(1, japanese, &character) == 0 && character == japanese + 3, "characters are counted, not bytes"); + Require(sceNpUtilGetNthChar(10, "abc", &character) == 0 && *character == '\0', "counting stops at the end"); + Require(sceNpUtilGetNthChar(2, "a\x01b", &character) == NpUtilInvalidCharacter, "control characters are refused"); + + char url[32] = {}; + Require(sceNpUtilHttpUrlEncode(url, sizeof(url), "a b=c*", 6, &length) == 0 && Equals(url, "a+b%3Dc*") && length == 9, + "form encoding keeps the safe set and escapes the rest"); + Require(sceNpUtilHttpUrlEncode(nullptr, 0, "@", 1, &length) == 0 && length == 4, "a null buffer reports the encoded size"); + Require(sceNpUtilHttpUrlEncode(url, 3, "@", 1, &length) == NpUtilBufferTooSmall, "encoding needs room for the terminator"); +} + +void TestTitleIds() { + NpTitleIdParts parts {{'A', 'B', 'C', 'D', '1', '2', '3', '4', '5'}, 0, 7}; + char titleId[13] = {}; + Require(sceNpUtilBuildTitleId(&parts, titleId, sizeof(titleId)) == 0 && Equals(titleId, "ABCD12345_07"), "the index is two digits"); + Require(sceNpUtilBuildTitleId(&parts, titleId, 12) == NpUtilBufferTooSmall, "a title id needs 13 bytes"); + parts.index = 100; + Require(sceNpUtilBuildTitleId(&parts, titleId, sizeof(titleId)) == NpUtilOutOfRange, "the index is at most 99"); +} + +void TestLanguagesAndDebugIds() { + char code[6] = {}; + char tag[36] = {}; + Require(sceNpUtilGetNpLanguageCode(1, code) == 0 && Equals(code, "en"), "language 1 is English"); + Require(sceNpUtilGetNpLanguageCode2(20, tag) == 0 && Equals(tag, "es-419"), "language 20 is Latin American Spanish"); + Require(Equals(sceNpUtilGetNpLanguageCodeStr(11), "zh-CN") && Equals(sceNpUtilGetNpLanguageCode2Str(30), "uk-UA"), + "language strings are returned directly"); + Require(sceNpUtilGetNpLanguageCode(31, code) == NpUtilInvalid && sceNpUtilGetNpLanguageCodeStr(31) == nullptr, + "unknown languages are refused"); + Require(sceNpUtilGetNpLanguageCode2(0, nullptr) == NpUtilInvalid, "a language code needs a buffer"); + const std::uint32_t first = sceNpDbgAssignDebugId(); + Require(first != 0 && sceNpDbgAssignDebugId() == first + 1, "debug ids count up"); +} + +void TestBase64() { + const std::uint8_t payload[] = {1, 2, 3, 4, 5}; + char text[16] = {}; + Require(sceNpBase64Encoder(payload, text, 5) == 8 && Equals(text, "AQIDBAU="), "base64 pads the last group"); + Require(sceNpBase64Encoder(payload, text, 0) == 0, "an empty payload encodes to nothing"); + Require(sceNpBase64GetDecodeSize("AQIDBAU=", 8) == 5 && sceNpBase64GetDecodeSize("AQ==", 4) == 1, + "the decoded size follows the padding"); + Require(sceNpBase64GetDecodeSize("", 0) == NpInvalidFormat && sceNpBase64GetDecodeSize("AQI", 3) == NpInvalidFormat, + "base64 needs whole groups"); + Require(sceNpBase64GetDecodeSize("AQ=A", 4) == NpInvalidCharacter && sceNpBase64GetDecodeSize("A=AAAQID", 8) == NpInvalidCharacter, + "padding is only allowed at the end"); + std::uint8_t bytes[8] = {}; + Require(sceNpBase64Decoder("AQIDBAU=", 8, bytes, sizeof(bytes)) == 5 && std::memcmp(bytes, payload, 5) == 0, + "base64 decodes back to the payload"); + Require(sceNpBase64Decoder("AQID", 4, bytes, 2) == NpInvalidFormat, "decoding refuses a short buffer"); + Require(sceNpBase64Decoder("AQ?D", 4, bytes, 3) == NpInvalidCharacter, "decoding refuses characters outside the alphabet"); + + const std::uint8_t url[] = {0xFB, 0x00}; + std::memset(text, 0, sizeof(text)); + Require(sceNpBase64UrlEncoder(url, text, 1) == 2 && Equals(text, "-w"), "base64url leaves out the padding"); + Require(sceNpBase64UrlEncoder(url, text, 2) == 3 && Equals(text, "-wA"), "base64url writes three characters for two bytes"); + Require(sceNpBase64UrlGetDecodeSize("-wA", 3) == 2 && sceNpBase64UrlGetDecodeSize("-w==", 4) == 1, + "base64url accepts padded and unpadded tails"); + Require(sceNpBase64UrlGetDecodeSize("", 0) == NpInvalidCharacter && sceNpBase64UrlGetDecodeSize("AAAAA", 5) == NpInvalidCharacter, + "a single trailing character cannot be decoded"); + Require(sceNpBase64UrlGetDecodeSize("+w==", 4) == NpInvalidCharacter, "base64url refuses the standard alphabet"); + Require(sceNpBase64UrlDecoder("-wA", 3, bytes, 2) == 2 && bytes[0] == 0xFB && bytes[1] == 0, "base64url decodes a short tail"); + Require(sceNpBase64UrlDecoder("-w", 2, bytes, 0) == NpInvalidFormat, "base64url decoding refuses a short buffer"); +} + +void TestNumbers() { + Require(sceNpHexToInt('a') == 10 && sceNpHexToInt('F') == 15 && sceNpHexToInt('g') == -1, "hex digits are read"); + Require(sceNpIntToHex(10, false) == 'a' && sceNpIntToHex(15, true) == 'F' && sceNpIntToHex(9, true) == '9', "hex digits are written"); + + char text[32] = {}; + std::size_t length = 0; + Require(sceNpUInt32ToStr(text, sizeof(text), 255, 16, 3, &length) == 0 && Equals(text, "0XFF") && length == 4, + "the hex prefix follows the case flag"); + Require(sceNpUInt32ToStr(text, sizeof(text), 8, 8, 2, &length) == 0 && Equals(text, "010") && length == 3, "octal gets a zero prefix"); + Require(sceNpUInt32ToStr(text, sizeof(text), 0, 10, 4, &length) == 0 && Equals(text, "0") && length == 1, "zero has no sign"); + Require(sceNpUInt32ToStr(text, 4, 1234, 10, 0, &length) == NpBufferTooSmall, "the terminator must fit"); + Require(sceNpUInt32ToStr(text, sizeof(text), 1, 37, 0, &length) == NpInvalidArgument, "the radix is limited to 36"); + Require(sceNpUInt32ToStr(nullptr, 0, 42, 10, 4, &length) == 0 && length == 3, "a null buffer reports the length"); + Require(sceNpInt32ToStr(text, sizeof(text), -2147483647 - 1, 10, 0, &length) == 0 && Equals(text, "-2147483648"), + "a negative 32-bit value always has a sign"); + Require(sceNpInt32ToStr(text, 3, -42, 10, 0, &length) == NpBufferTooSmall, "the sign takes buffer space"); + Require(sceNpInt64ToStr(text, sizeof(text), -42, 10, 0, &length) == 0 && Equals(text, "42") && length == 2, + "a 64-bit value only gets a sign with the sign flag"); + Require(sceNpInt64ToStr(text, sizeof(text), -42, 10, 4, &length) == 0 && Equals(text, "-42"), "the sign flag writes the minus"); + Require(sceNpUInt64ToStr(text, sizeof(text), UINT64_MAX, 16, 3, &length) == 0 && Equals(text, "0XFFFFFFFFFFFFFFFF") && length == 18, + "the full 64-bit range is formatted"); + + std::int32_t value32 = 0; + std::size_t parsed = 0; + Require(sceNpStrToInt32("-567abc", &value32, 10, &parsed) == 0 && value32 == -567 && parsed == 4, "parsing stops at the first non-digit"); + Require(sceNpStrToInt32("0x1A", &value32, 0, &parsed) == 0 && value32 == 26, "base zero follows the prefix"); + Require(sceNpStrToInt32("invalid", &value32, 10, &parsed) == NpOutOfRange, "no digits is an error"); + parsed = 0; + Require(sceNpStrToInt32("3000000000", &value32, 10, &parsed) == NpOutOfRange && parsed == 10, + "the length is reported before the 32-bit range check"); + std::int64_t value64 = 0; + Require(sceNpStrToInt64("-9223372036854775808", &value64, 10, nullptr) == 0 && value64 == INT64_MIN, "the 64-bit minimum parses"); + Require(sceNpStrToInt64("99999999999999999999", &value64, 10, nullptr) == NpOutOfRange, "64-bit overflow is an error"); + std::uint32_t unsigned32 = 0; + Require(sceNpStrToUInt32("4294967295", &unsigned32, 10, nullptr) == 0 && unsigned32 == UINT32_MAX, "the 32-bit maximum parses"); + Require(sceNpStrToUInt32("4294967296", &unsigned32, 10, nullptr) == NpOutOfRange, "32-bit unsigned overflow is an error"); + std::uint64_t unsigned64 = 0; + Require(sceNpStrToUInt64("0xFFFFFFFFFFFFFFFF", &unsigned64, 0, nullptr) == 0 && unsigned64 == UINT64_MAX, "the 64-bit maximum parses"); + Require(sceNpStrToUInt64("18446744073709551616", &unsigned64, 10, nullptr) == NpOutOfRange, "64-bit unsigned overflow is an error"); +} + +void TestHexAndCopies() { + const std::uint8_t data[] = {0x0A, 0xFE}; + char text[8] = {}; + Require(sceNpStrBuildHex(text, 5, data, 2, false) == 0 && Equals(text, "0afe"), "bytes are written as lowercase hex"); + Require(sceNpStrBuildHex(text, 5, data, 2, true) == 0 && Equals(text, "0AFE"), "bytes are written as uppercase hex"); + Require(sceNpStrBuildHex(text, 4, data, 2, false) == NpBufferTooSmall, "hex output needs room for the terminator"); + std::uint8_t bytes[4] = {}; + Require(sceNpStrParseHex("0aFE", bytes, 2) == 0 && bytes[0] == 0x0A && bytes[1] == 0xFE, "hex is read in either case"); + Require(sceNpStrParseHex("0AF", bytes, 2) == NpInvalidArgument, "odd hex is refused"); + Require(sceNpStrParseHex("0AFE", bytes, 1) == NpBufferTooSmall, "hex parsing checks the output size"); + Require(sceNpStrParseHex("0AG1", bytes, 2) == NpInvalidArgument, "non-hex characters are refused"); + Require(sceNpStrnParseHex("DEADBEEF1234", 8, bytes, 4) == 0 && bytes[0] == 0xDE && bytes[3] == 0xEF, "a hex prefix is read"); + + char copy[8] = {}; + Require(sceNpStrcpyToBuf(copy, 6, "Hello") == 0 && Equals(copy, "Hello"), "a string that fits is copied"); + Require(sceNpStrcpyToBuf(copy, 5, "Hello") == NpBufferTooSmall, "a string without room for its terminator is refused"); + Require(sceNpStrncpyToBuf(copy, 6, "Hello World", 5) == 0 && Equals(copy, "Hello"), "the length limit sizes the copy"); + Require(sceNpStrncpyToBuf(copy, 5, "Hello World", 5) == NpBufferTooSmall, "the limited string still needs a terminator"); +} + +} + +int main() { + TestAtomics(); + TestSemaphoresAndFlags(); + TestThreads(); + TestHeaps(); + TestBase64(); + TestNumbers(); + TestHexAndCopies(); + TestCallouts(); + TestIds(); + TestLanguagesAndDebugIds(); + TestJsonText(); + TestTitleIds(); + std::puts("guest np common runtime: PASS"); +} diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index e6e51903bb..32efbb235e 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -2,113 +2,28 @@ ### Unknown function info -The 129 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. +The 35 declarations below use names mapped from the local `libSceNpCommon.sprx` export table. Each NID was verified against that table and recomputed from its name with `tools/nid_names.py`. The public [PS5 3.20 libSceNpCommon export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/main/libSceNpCommon.c) contains all 150 symbol names. No prototype or behavior is claimed. Each declaration uses the no-parameter unknown-signature convention solely for import resolution and throws if called. -- [sceNpAllocateKernelMemoryNoAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `MWPOkqzYss0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpAllocateKernelMemoryWithAlignment](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `gMlY6eewr-c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpArchInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `jGF+MaB4b-M`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `UskWpVWxSvg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpAtomicCas32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+9+kKMY9YIw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpAtomicDec32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Yohe0MMDfj0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpAtomicInc32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pfJgSA4jO3M`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64Decoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `l67qBmMmKP4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64Encoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pu39pU8UgCo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64GetDecodeSize](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `a5IfPlpchXI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64UrlDecoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `moGcgMNTHvQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64UrlEncoder](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `IeNj+OcWgU8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpBase64UrlGetDecodeSize](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `7BjZKcN+oZ4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCalloutInitCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9+m5nRdJ-wQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCalloutStartOnCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fClnlkZmA6k`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lpr66Gby8dQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCalloutStopOnCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `in19gH7G040`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCalloutTermCtx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `AqJ4xkWsV+I`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCancelEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kb2thTuS8t8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpClearEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9pLoHoPMxeg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `+nmn+Z0nWDo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpCloseSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `8hPzfjZzV88`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bsjWg59A7aE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `bAHIOyNnx5Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fZShld2PQ7w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCreateEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `6jFWpAfqAcc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCreateSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LHZtCT2W1Pw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fhJ5uKzcn0w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpDbgAssignDebugId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `90pmGqDK4BI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Etq15-l9yko`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpDbgDumpText](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZaKa5x61hGA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpDeleteEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `sjnIeFCuTD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpDeleteSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPrF2nGPBXQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpFreeKernelMemory](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vjwlDmsGtME`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QmDEFikd3VA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetProcessId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Z3mnqcGmf8E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pJlGhXEt5CU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGetSdkVersionUInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ljqnF0hmLjo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGlobalHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-gN6uE+zWng`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGlobalHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VUHUasztbUY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGlobalHeapGetAllocatorExPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `P4YpPziLBd4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpGlobalHeapGetAllocatorPtr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DI5n4aOdxmk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `wVdn78HKc30`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `lvek8w7yqyE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHeapGetStat](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `2jdHoPpS+W0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `B+yGIX1+BTI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `evz0-93ucJc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpHexToInt](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Hvpr+otU4bo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpInt32ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5y0wMPQkaeU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `HoPC33siDD4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntGetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `G6qytFoBJ-w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIntIsOnlineIdString](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fY4XQoA20i8`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpIntSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X6emt+LbSEI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIntToHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `TWPY1x1Atys`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIpc2ClientInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kgDwlmy78k0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpIpc2ClientTerm](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CI2p6Viee9w`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vJGDnNh4I0g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RgfCYkjW7As`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseBufInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SnAdybtBK3o`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `p5ZkSMRR7AU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseExInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `nhgjiwPUIzI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpJsonParseInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `teVnFAL6GNY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `zNb6IxegrCE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `++eqYdzB8Go`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondSignal](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Xkn6VoN-wuQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondSignalAll](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FJ4DCt8VzVE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondSignalTo](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Bwi+EP8VQ+g`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpLwCondWait](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ExeLuE3EQCQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMemoryHeapDestroy](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `dfXSH2Tsjkw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMemoryHeapGetAllocator](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FaMNvjMA6to`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMemoryHeapGetAllocatorEx](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xHAiSVEEjSI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpMemoryHeapInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kZizwrFvWZY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpOpenEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `5DkyduAF2rs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpOpenSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-blITIdtUd0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpPanic](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZoXUrTiwKNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpPollEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `9YmBJ8KF9eI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpPollSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xmF0yIF4iXc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpRtcConvertToPosixTime](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `VMjIo2Z-aW0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpRtcFormatRFC3339](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `W0YWLVDndx0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpRtcParseRFC3339](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `LtkeQwMIEWY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpServerErrorJsonGetErrorCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `0lZHbA-HRD0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpServerErrorJsonMultiGetErrorCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `cRabutqUG7c`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpServerErrorJsonParse](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `WSQxnAVLKgw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpServerErrorJsonParseInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `UbStlMKTBeU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpServerErrorJsonParseMultiInit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `hbe+DdooIi4`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpSetEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `29ftOGIrUCo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpSetPlatformType](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `m9JzZSoDVFY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpSignalSema](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `-W28+9p1CKI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrBuildHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `i5TP5NLmkoQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrParseHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `DUHzVPNlugg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrToInt32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fElyBSn-l24`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrToInt64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `CwqYdG4TrjA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrToUInt32](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `uj86YxCYid0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrToUInt64](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Ted2YU9lv94`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrcpyToBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ivnnssCwjGI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrnParseHex](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `h1SWCcBdImo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpStrncpyToBuf](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `PHrpHMSU8Cs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpThreadGetId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `yvaNTRiKXmo`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUInt32ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `rRN89jBArEM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUInt64ToStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `QjNUYQbGoHA`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUserGetUserIdList](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Gh74vNl06sg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilBuildTitleId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `N3tAHlBnowE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilCanonicalizeNpIdForPs4](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4mEAk-UKVNw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilCanonicalizeNpIdForPsp2](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `N3FB4r8JoRE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilCmpAccountId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `xPRHNaD3kTc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetDateSetAuto](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `owm52JoZ8uc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetDbgCommerce](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `1Gfhi+tZ9IE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetEnv](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `kBON3bAtfGs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. @@ -116,26 +31,27 @@ The 129 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpUtilGetFakeRateLimit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `O86rgZ2azfg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetIgnoreNpTitleId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `FrxliFYAO8Y`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetNpDebug](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `GRvK1ZE+FEQ`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilGetNpLanguageCode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `OFiFmfsADas`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilGetNpLanguageCode2](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `X9CqyP164Hc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilGetNpLanguageCode2Str](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `Fxux7Ob+Ynk`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilGetNpLanguageCodeStr](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `RfiA17kV+xs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetNpTestPatch](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `OA8f3KF9JsM`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilGetNthChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `KCk4OGu8+sc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetShareTitleCheck](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `fB5hE65pzbU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetSystemLanguage](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `SXUNKr9Zkv0`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetTrcNotify](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `AjzLvR0g5Zs`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetWebApi2FakeRateLimit](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `pmHBFJyju9E`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetWebApi2FakeRateLimitTarget](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ZRxKp9vjcNc`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilGetWebTraceSetting](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4CqfNm3pisU`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilHttpUrlEncode](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `ajoqGz0D9Dw`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. - [sceNpUtilJidToNpId](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `458yjI+OECI`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilJsonEscape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `EftEB4kmkSg`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `vj04qzp7uKY`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. -- [sceNpUtilJsonUnescape](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) (NID `4YJ5gYtRAAE`) - the signature and behavior are unknown. The no-parameter declaration only resolves the import; it makes no ABI claim. Calls throw through `NotImplemented_nid_no_patch`. ### Functional - [Online ID and NpId utilities](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - `sceNpCmpOnlineId`, `sceNpCmpNpIdInOrder`, `sceNpGetPlatformType` and `sceNpIntIsValidOnlineId` are not measured on a console; their signatures, checks and results follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp), including the platform tags read from `opt[4..7]` and the 3 to 16 character online ID rule. - [Mutex, condition and thread wrappers](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - the `sceNpMutex*`, `sceNpLwMutex*`, `sceNpCond*` and `sceNpJoinThread` wrappers forward to the libkernel `scePthread*` calls as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp) and are not measured on a console. Only `EBUSY` from try-lock and `ETIMEDOUT` from a timed wait are mapped to NP errors; every other kernel error is returned unchanged. The `flags` argument of `sceNpCondInit` is ignored. - [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - writes the libkernel system software version as `major.minor` into an 8-byte buffer without checking it, as shadPS4 does; that version is the fixed `1.00`, not the SDK version a title was built with. +- [The exports in Runtime.cpp](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [Strings.cpp](../../../core/libs/prx/libSceNpCommon/Strings.cpp) follow `libSceNpCommon.sprx` from firmware 12.00, read from disassembly and not measured on a console. +- [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) always applies an explicit priority when one is given; the firmware picks between explicit and inherited scheduling from the title's compiled SDK version, which is not available here. +- [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) draws from a 32-bit Mersenne Twister seeded once per process; the console uses SFMT-19937 with a seed set inside the module, so the sequences differ. +- [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) print the same information as the console in a different layout. +- [The string-to-integer exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) clear `errno` before parsing; the firmware reads whatever `errno` held before the call when the result saturates. +- [The integer-to-string exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) return `0x80558008` when the hexadecimal prefix does not fit; the firmware writes the prefix without checking the size. [The base64 decoders](../../../core/libs/prx/libSceNpCommon/Strings.cpp) treat bytes above 0x7F as invalid; the firmware indexes its table with them as signed values. [sceNpUtilJsonGetOneChar](../../../core/libs/prx/libSceNpCommon/Strings.cpp) checks that a `\u` escape and a surrogate pair fit in the input; the firmware reads past it. +- [sceNpUtilGetNpDebug, sceNpUtilGetNpTestPatch, sceNpUtilGetTrcNotify, sceNpUtilGetIgnoreNpTitleId, sceNpUtilGetFakeDisplayNameMode, sceNpUtilGetWebTraceSetting, sceNpUtilGetWebApi2FakeRateLimit, sceNpUtilGetWebApi2FakeRateLimitTarget, sceNpUtilGetFakeRateLimit, sceNpUtilGetDbgCommerce, sceNpUtilGetSystemLanguage, sceNpUtilGetEnv, sceNpUtilGetDateSetAuto and sceNpUtilGetShareTitleCheck](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) read system and debug registry keys through libSceRegMgr, whose `sceRegMgrNonSysGetInt` and string reads still throw; they throw until those exist. +- [sceNpOpenSema, sceNpCloseSema and sceNpCloseEventFlag](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) forward to the libkernel open and close calls for named semaphores and event flags, which still throw. +- [sceNpSetPlatformType, sceNpIntGetPlatformType, sceNpIntSetPlatformType, sceNpUtilCanonicalizeNpIdForPs4, sceNpUtilCanonicalizeNpIdForPsp2, sceNpUtilJidToNpId and sceNpGetNavSdkVersion](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) depend on console platform tags, network domain names or a version string of the console; they throw. +- [sceNpJsonParse, sceNpJsonParseInit, sceNpJsonParseBuf, sceNpJsonParseBufInit, sceNpJsonParseEx, sceNpJsonParseExInit and the sceNpServerErrorJson exports](../../../core/libs/prx/libSceNpCommon/PendingExports.cpp) are a streaming, schema-checked JSON parser whose event and error rules are not yet specified; they throw. From 99c195e79a7763aea3a376afb2d3f58b2c401947 Mon Sep 17 00:00:00 2001 From: Retr0 Date: Tue, 20 Oct 2026 19:00:50 +0300 Subject: [PATCH 122/318] fix(libc): leave tzname[1] as three spaces for a zone without daylight saving time FreeBSD 11 localtime.c initializes tzname to WILDABBR (" ") and settzname() resets both entries to it before filling them from the zone's types, so a zone with no daylight saving type keeps " " in tzname[1]. Windows left it empty and Linux repeated tzname[0]. --- core/libs/prx/libc/src/Time.cpp | 24 +++++++++++++++++------- core/libs/tests/GuestTimeConversion.cpp | 8 +++++--- docs/dev/debt/libc.md | 2 +- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index c3499f8047..afb22bd316 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -183,6 +183,7 @@ std::recursive_mutex& zoneMutex() { } char** HostZoneNames(); +char* GuestZoneName(int index); void applyGuestZone() { static bool initialized = false; @@ -205,9 +206,8 @@ void applyGuestZone() { #else ::tzset(); #endif - char** names = HostZoneNames(); - tzname_nid_postfix[0] = names[0]; - tzname_nid_postfix[1] = names[1]; + tzname_nid_postfix[0] = GuestZoneName(0); + tzname_nid_postfix[1] = GuestZoneName(1); } int& zoneDepth() { @@ -515,11 +515,22 @@ char** HostZoneNames() { return ::tzname; #endif } + +char* GuestZoneName(int index) { + static char unnamed[] = " "; + char** names = HostZoneNames(); +#ifdef _WIN32 + const bool daylightSaving = _daylight != 0; +#else + const bool daylightSaving = ::daylight != 0; +#endif + return index == 1 && !daylightSaving ? unnamed : names[index]; +} } extern "C" { -char* tzname_nid_postfix[2] = {HostZoneNames()[0], HostZoneNames()[1]}; +char* tzname_nid_postfix[2] = {GuestZoneName(0), GuestZoneName(1)}; int64_t timezone_nid_postfix = 0; int daylight_nid_postfix = 0; @@ -533,9 +544,8 @@ int APS5_VABI ___mb_cur_max_nid_postfix(); void APS5_VABI tzset_nid_postfix(void) { const ZoneScope zone; - char** names = HostZoneNames(); - tzname_nid_postfix[0] = names[0]; - tzname_nid_postfix[1] = names[1]; + tzname_nid_postfix[0] = GuestZoneName(0); + tzname_nid_postfix[1] = GuestZoneName(1); const int64_t now = libc_time_nid_postfix(nullptr); GuestTm universal{}; GuestTm local{}; diff --git a/core/libs/tests/GuestTimeConversion.cpp b/core/libs/tests/GuestTimeConversion.cpp index 7b11ebe2ef..f3fca8ad13 100644 --- a/core/libs/tests/GuestTimeConversion.cpp +++ b/core/libs/tests/GuestTimeConversion.cpp @@ -359,7 +359,8 @@ static void CheckGuestZone() { Require(setenv_nid_postfix("TZ", "EST5", 1) == 0, "Could not set the guest time zone without tzset"); Require(localtime_s_nid_postfix(&moment, &result) == &result && result.tm_hour == 19 && result.tm_gmtoff == -18000, "localtime ignored a guest TZ change made without tzset"); - Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0, "Implicit guest TZ update did not refresh tzname"); + Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0 && std::strcmp(tzname_nid_postfix[1], " ") == 0, + "Implicit guest TZ update did not refresh tzname"); Require(setenv_nid_postfix("TZ", "UTC-2", 1) == 0, "Could not restore the guest time zone"); tzset_nid_postfix(); } @@ -527,8 +528,9 @@ int main() { GuestTm shifted{}; Require(localtime_s_nid_postfix(&epoch, &shifted) == &shifted && shifted.tm_hour == 5 && shifted.tm_gmtoff == 18000, "tzset did not pick up the new TZ"); - Require(tzname_nid_postfix[0] != nullptr && std::strcmp(tzname_nid_postfix[0], "UTC") == 0 && tzname_nid_postfix[1] != nullptr, - "tzname does not name the zone tzset set"); + Require(tzname_nid_postfix[0] != nullptr && std::strcmp(tzname_nid_postfix[0], "UTC") == 0 && tzname_nid_postfix[1] != nullptr && + std::strcmp(tzname_nid_postfix[1], " ") == 0, + "tzname does not name the zone tzset set and leave the daylight name blank"); Require(setenv_nid_postfix("TZ", "EST5EDT", 1) == 0, "Could not set the guest time zone"); tzset_nid_postfix(); Require(std::strcmp(tzname_nid_postfix[0], "EST") == 0 && std::strcmp(tzname_nid_postfix[1], "EDT") == 0, diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 581f589299..bfc4a0c442 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -117,7 +117,7 @@ - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console - [Colons in guest names](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows (NTFS) a guest name containing `:` is not a plain file name: `name:stream` opens or creates the alternate data stream `stream` of the file `name`. Creating `/savedata0/C:file` makes an empty file `C` whose stream `file` holds the data, and `stat` of `/savedata0/C` then reports a regular file. `mkdir` of a name containing `:`, such as `C:` or `dir:x`, fails with EINVAL. `name:stream` already behaved this way before drive-letter components were kept under their mount -- [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. A zone with no daylight-saving time has `tzname[1]` empty on Windows and equal to `tzname[0]` on Linux, where FreeBSD leaves three spaces. The console's names are not known +- [tzname](../../../core/libs/prx/libc/src/Time.cpp) (libc) points at the host's zone names, read at load and again by `tzset`: the abbreviations a `TZ` string gives (`EST5EDT` is `EST` and `EDT` on both hosts), but on Windows without `TZ` the CRT's full names such as `GMT Standard Time`, which ICU's [uprv_tzname](https://github.com/unicode-org/icu/blob/457157a92aa053e632cc7fcfd0e12f8a943b2d11/icu4c/source/common/putil.cpp#L1244-L1264) (ES-DE in PPSA99169) cannot map to a zone ID. The console's names are not known - [Trailing slash on a mount alias](../../../core/libs/prx/libc/src/General.cpp) (libc) - on Windows a trailing slash after a file name under a mount alias such as `/savedata0` is still dropped, so `unlink`, `rename`, `open` and the other path calls act on the file where FreeBSD 11.4's `lookup()` fails with `ENOTDIR`; Linux and macOS pass the slash to the host, as for paths outside an alias - [localtime, mktime](../../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule. A time outside that range is converted a whole number of 400-year Gregorian cycles away, where the calendar repeats, so it gets the rule the CRT applies to that year and not the zone's historical one. On macOS `mktime` converts through the host's `mktime`, which refuses every year before 1900. A local time before 1900 is instead solved for directly: the offset `localtime_r` gives for a candidate time, which follows the zone's history (local mean time before its first transition), is subtracted from the local fields, and the step is repeated until the offset holds. `tm_isdst` is not consulted there, so a time with `tm_isdst` set where the zone had no daylight offset is taken as standard time. FreeBSD's [`time1`](https://github.com/freebsd/freebsd-src/blob/stable/11/contrib/tzcode/stdtime/localtime.c#L2036) would instead shift it by the difference between one of the zone's daylight types and a standard one. `gmtime` converts every time whose year fits an `int` on both hosts. - [strftime and asctime](../../../core/libs/prx/libc/src/Time.cpp) (libc) follow FreeBSD 11 [strftime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdtime/strftime.c) and [asctime.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/contrib/tzcode/stdtime/asctime.c) with the C locale on every host, not measured on the console; other locales are not implemented. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%Z` is `tm_zone`, or `tzname[tm_isdst != 0]` when it is null and `tm_isdst` is not negative. The two-digit year warning FreeBSD writes to `stderr` when `CHECK_STRFTIME_FORMATS_FOR_TWO_DIGIT_YEARS` is set is not written From 9863f8b64120395eb6137fd1f247fb783fe9cac3 Mon Sep 17 00:00:00 2001 From: capmare Date: Wed, 21 Oct 2026 14:13:06 +0200 Subject: [PATCH 123/318] fix(shader): give the frontend's call analysis the dispatch's wave size AcquireFrontend built the frontend's SwappcInfo with a wave size of 0, while the driver's LinkUserDataCallees passes the dispatch's wave size. A wave64 v_cmp_e64 that reuses a call's link pair after the return, followed by a v_cndmask_b32_e64 that reads it, was refused as escaping the call/return model. The frontend now receives the dispatch's wave size, and a frontend built for one wave size is not reused for another. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 27 +++++++++++++++++-- core/shader/recompiler/Recompiler.cpp | 4 +-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a754f42271..9a4bf7b90c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -168,7 +168,6 @@ void verifyFrontendIdentity() { require((preparation.AcquireFrontend(partial) == first) == reuse, "frontend pair: partial-thread state changed the frontend identity"); auto target = request; target.target.subgroupSize = 64u; - target.context.waveSize = 32u; target.context.compute->numThreads = {32u, 2u, 1u}; require((preparation.AcquireFrontend(target) == first) == reuse, "frontend pair: translation-only state changed the frontend identity"); const std::array changedUserData{1u, 2u}; @@ -197,6 +196,10 @@ void verifyFrontendIdentity() { changed.context.userDataBaseRegister = 4u; verifyMiss(changed, "frontend pair: changed user-data base reused the frontend"); changed = request; + changed.context.waveSize = 32u; + changed.context.compute->numThreads = {32u, 2u, 1u}; + verifyMiss(changed, "frontend pair: a different wave size reused the frontend"); + changed = request; changed.context.userData = std::span(userData).first(1u); verifyMiss(changed, "frontend pair: changed user-data count reused the frontend"); changed = request; @@ -222,6 +225,25 @@ void verifyFrontendIdentity() { require((preparation.AcquireFrontend(request) == first) == reuse, "frontend pair: a failed miss replaced the prior frontend"); } +void verifyFrontendWaveSizeCalls() { + using namespace ShaderRecompiler; + const std::vector code{ + 0xbb0e0005u, + 0xd4c2000eu, 0x00020300u, + 0xd5010002u, 0x003a0300u, + 0xbf820001u, + 0xbe80200eu, + 0xbf810000u}; + const std::array userData{}; + for (const std::uint32_t waveSize : {32u, 64u}) { + auto request = FrontendRequest(code, userData); + request.context.waveSize = waveSize; + request.context.compute->numThreads = {waveSize, 1u, 1u}; + ShaderPreparationContext preparation; + require(preparation.AcquireFrontend(request) != nullptr, "frontend wave size: a mask write that reuses the link pair and its v_cndmask read were refused"); + } +} + void verifyFrontendProgram(const ShaderRecompiler::IrProgram& reused, const ShaderRecompiler::IrProgram& fresh) { using namespace ShaderRecompiler; require(ProgramToString(reused) == ProgramToString(fresh), "frontend pair: reused frontend changed the IR"); @@ -256,6 +278,7 @@ void verifyFrontendProgram(const ShaderRecompiler::IrProgram& reused, const Shad void verifyFrontendPair() { using namespace ShaderRecompiler; verifyFrontendIdentity(); + verifyFrontendWaveSizeCalls(); const bool reuse = std::getenv("APS5_NO_PERF_FRONTEND_PAIR") == nullptr; const std::vector code{0xe0700000u, 0x80000000u, 0xbf810000u}; const std::array buffer{0x10000000u, 0u, 0x40u, 0x00027facu}; @@ -318,7 +341,7 @@ void verifyFrontendPair() { const auto freshChanged = PrepareResourceProgram(translated); verifyFrontendProgram(changed, freshChanged); require(changed.WaveSize() != freshFull.WaveSize(), "frontend pair: changed translation inputs were ignored"); - require((preparation.AcquireFrontend(request) == retained) == reuse, "frontend pair: translating a variant replaced the frontend"); + require(preparation.AcquireFrontend(request) != retained, "frontend pair: a variant with another wave size reused the frontend"); } for (const bool partialFirst : {false, true}) { diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ad94823d64..c1a26afc7c 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -169,7 +169,7 @@ struct PreparedControlFlow { bool Matches(const RecompileRequest& request, const SwappcInfo& swappcInfo) const { return stage == request.shader.stage && mergedHull == MergedHullStage(request) && swappc.fetchCallAllowed == swappcInfo.fetchCallAllowed && - swappc.userDataBaseRegister == swappcInfo.userDataBaseRegister && swappc.userDataCount == swappcInfo.userDataCount && + swappc.userDataBaseRegister == swappcInfo.userDataBaseRegister && swappc.userDataCount == swappcInfo.userDataCount && swappc.waveSize == swappcInfo.waveSize && std::ranges::equal(fallbacks, swappcInfo.linkedCallFallbacks) && std::ranges::equal(code, request.shader.code); } @@ -189,7 +189,7 @@ struct PreparedControlFlow { std::shared_ptr ShaderPreparationContext::AcquireFrontend(const RecompileRequest& request) { const auto inputInfo = RequestInputInfo(request); - const SwappcInfo swappc{inputInfo.vertex != nullptr, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), 0u, request.context.linkedCallFallbacks}; + const SwappcInfo swappc{inputInfo.vertex != nullptr, request.context.userDataBaseRegister, static_cast(request.context.userData.size()), request.context.waveSize, request.context.linkedCallFallbacks}; static const bool reuse = std::getenv("APS5_NO_PERF_FRONTEND_PAIR") == nullptr; if (reuse && frontend != nullptr && frontend->Matches(request, swappc)) { return frontend; From b50e5c7a4d02ec85d90edb3474319f9ebfb71b1e Mon Sep 17 00:00:00 2001 From: jacob6769 Date: Wed, 21 Oct 2026 21:29:23 +0900 Subject: [PATCH 124/318] feat(libSceCesCs): implement the EUC-KR, UHC and Big5 scalar conversions Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceCesCs/Export.cpp | 291 ++++++++++++++++++- core/libs/prx/libSceCesCs/PendingExports.cpp | 225 -------------- core/libs/tests/GuestCesDbcs.cpp | 288 ++++++++++++++++++ docs/dev/debt/libSceCesCs.md | 4 +- 4 files changed, 578 insertions(+), 230 deletions(-) diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 25dcb94514..929f6427cd 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -501,14 +502,49 @@ int fromMbcs(const void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_ return 0; } -int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint8_t* utf8, uint32_t utf8Max, uint32_t* utf8Len) { +constexpr bool NATIVE_BIG_ENDIAN = std::endian::native == std::endian::big; + +template +uint32_t loadUnit(const Unit* source) { + const auto* bytes = reinterpret_cast(source); + uint32_t code = 0; + for (uint32_t i = 0; i < sizeof(Unit); ++i) code |= uint32_t(bytes[i]) << (8 * (BigEndian ? sizeof(Unit) - 1 - i : i)); + return code; +} + +template +void storeUnit(Unit* target, uint32_t code) { + auto* bytes = reinterpret_cast(target); + for (uint32_t i = 0; i < sizeof(Unit); ++i) bytes[i] = static_cast(code >> (8 * (BigEndian ? sizeof(Unit) - 1 - i : i))); +} + +template +int decodeUtf16(const uint16_t* utf16, uint32_t utf16Max, uint32_t& length, uint32_t& code) { + code = loadUnit(utf16); + length = 1; + if (code >= 0xDC00 && code <= 0xDFFF) return CES_ERROR_INVALID_ENCODE; + if (code >= 0xD800 && code <= 0xDBFF) { + if (utf16Max < 2) return CES_ERROR_SRC_BUFFER_END; + const uint32_t low = loadUnit(utf16 + 1); + if (low < 0xDC00 || low > 0xDFFF) return CES_ERROR_INVALID_ENCODE; + code = 0x10000 + ((code - 0xD800) << 10) + low - 0xDC00; + length = 2; + } + return 0; +} + +int decodeScalar(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, const void* target, uint32_t& consumed, uint32_t& code) { if (!accepted) return CES_ERROR_INVALID_PARAMETER; if (!mbcs) return CES_ERROR_INVALID_SRC_BUFFER; if (mbcsMax == 0) return CES_ERROR_SRC_BUFFER_END; - if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; + if (!target) return CES_ERROR_INVALID_DST_BUFFER; + return decodeMbcs(profile, mbcs, mbcsMax, consumed, code); +} + +int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint8_t* utf8, uint32_t utf8Max, uint32_t* utf8Len) { uint32_t consumed = 0; uint32_t code = 0; - if (const int result = decodeMbcs(profile, mbcs, mbcsMax, consumed, code); result != 0) return result; + if (const int result = decodeScalar(accepted, profile, mbcs, mbcsMax, utf8, consumed, code); result != 0) return result; uint8_t encoded[3]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; @@ -518,6 +554,75 @@ int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t return 0; } +int mbcsToUcs2(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* ucs2) { + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decodeScalar(accepted, profile, mbcs, mbcsMax, ucs2, consumed, code); result != 0) return result; + *ucs2 = static_cast(code); + if (mbcsLen) *mbcsLen = consumed; + return 0; +} + +template +int mbcsToUtf16(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* utf16, uint32_t utf16Max, uint32_t* utf16Len) { + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decodeScalar(accepted, profile, mbcs, mbcsMax, utf16, consumed, code); result != 0) return result; + if (utf16Max < 1) return CES_ERROR_DST_BUFFER_END; + storeUnit(utf16, code); + if (mbcsLen) *mbcsLen = consumed; + if (utf16Len) *utf16Len = 1; + return 0; +} + +template +int mbcsToUtf32(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf32) { + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decodeScalar(accepted, profile, mbcs, mbcsMax, utf32, consumed, code); result != 0) return result; + storeUnit(utf32, code); + if (mbcsLen) *mbcsLen = consumed; + return 0; +} + +int storeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { + uint8_t encoded[3]; + uint32_t produced = 0; + if (const int result = encodeMbcs(profile, code, encoded, produced); result != 0) return result; + if (mbcsMax < produced) return CES_ERROR_DST_BUFFER_END; + std::memcpy(mbcs, encoded, produced); + if (mbcsLen) *mbcsLen = produced; + return 0; +} + +int scalarToMbcs(bool accepted, uint32_t code, const void* profile, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { + if (!accepted) return CES_ERROR_INVALID_PARAMETER; + if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; + if (code > 0x10FFFF || (code >= 0xD800 && code <= 0xDFFF)) return CES_ERROR_INVALID_ENCODE; + return storeMbcs(profile, code, mbcs, mbcsMax, mbcsLen); +} + +template +int utf32ToMbcs(bool accepted, const uint32_t* utf32, const void* profile, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { + if (!accepted) return CES_ERROR_INVALID_PARAMETER; + if (!utf32) return CES_ERROR_INVALID_SRC_BUFFER; + return scalarToMbcs(accepted, loadUnit(utf32), profile, mbcs, mbcsMax, mbcsLen); +} + +template +int unitsToMbcs(bool accepted, const Source* source, uint32_t sourceMax, uint32_t* sourceLen, const void* profile, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, Decode decode) { + if (!accepted) return CES_ERROR_INVALID_PARAMETER; + if (!source) return CES_ERROR_INVALID_SRC_BUFFER; + if (sourceMax == 0) return CES_ERROR_SRC_BUFFER_END; + if (!mbcs) return CES_ERROR_INVALID_DST_BUFFER; + uint32_t consumed = 0; + uint32_t code = 0; + if (const int result = decode(source, sourceMax, consumed, code); result != 0) return result; + if (const int result = storeMbcs(profile, code, mbcs, mbcsMax, mbcsLen); result != 0) return result; + if (sourceLen) *sourceLen = consumed; + return 0; +} + } extern "C" { @@ -674,4 +779,184 @@ int APS5_VABI sceCesUhcToUtf8(void* profile, uint8_t* uhcBuffer, uint32_t uhcMax return mbcsToUtf8(isUhc(profile), profile, uhcBuffer, uhcMax, uhcLength, utf8Buffer, utf8Max, utf8Length); } +int APS5_VABI sceCesEucKrToUcs2(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2) { + return mbcsToUcs2(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, ucs2); +} + +int APS5_VABI sceCesEucKrToUtf16(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucKrToUtf16be(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucKrToUtf16le(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesEucKrToUtf32(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return mbcsToUtf32(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesEucKrToUtf32be(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return mbcsToUtf32(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesEucKrToUtf32le(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint32_t* utf32) { + return mbcsToUtf32(isEucKr(profile), profile, eucBuffer, eucMax, eucSize, utf32); +} + +int APS5_VABI sceCesUhcToUcs2(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint16_t* ucs2) { + return mbcsToUcs2(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, ucs2); +} + +int APS5_VABI sceCesUhcToUtf16(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesUhcToUtf16be(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesUhcToUtf16le(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesUhcToUtf32(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint32_t* utf32) { + return mbcsToUtf32(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf32); +} + +int APS5_VABI sceCesUhcToUtf32be(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint32_t* utf32) { + return mbcsToUtf32(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf32); +} + +int APS5_VABI sceCesUhcToUtf32le(const void* profile, const uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize, uint32_t* utf32) { + return mbcsToUtf32(isUhc(profile), profile, uhcBuffer, uhcMax, uhcSize, utf32); +} + +int APS5_VABI sceCesBig5ToUcs2(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint16_t* ucs2) { + return mbcsToUcs2(isBig5(profile), profile, big5Buffer, big5Max, big5Size, ucs2); +} + +int APS5_VABI sceCesBig5ToUtf16(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesBig5ToUtf16be(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesBig5ToUtf16le(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { + return mbcsToUtf16(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf16Buffer, utf16Max, utf16Length); +} + +int APS5_VABI sceCesBig5ToUtf32(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint32_t* utf32) { + return mbcsToUtf32(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf32); +} + +int APS5_VABI sceCesBig5ToUtf32be(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint32_t* utf32) { + return mbcsToUtf32(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf32); +} + +int APS5_VABI sceCesBig5ToUtf32le(const void* profile, const uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size, uint32_t* utf32) { + return mbcsToUtf32(isBig5(profile), profile, big5Buffer, big5Max, big5Size, utf32); +} + +int APS5_VABI sceCesUcs2ToEucKr(uint16_t ucs2, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return scalarToMbcs(isEucKr(profile), ucs2, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf8ToEucKr(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return unitsToMbcs(isEucKr(profile), utf8Buffer, utf8Max, utf8Length, profile, eucBuffer, eucMax, eucSize, decodeUtf8); +} + +int APS5_VABI sceCesUtf16ToEucKr(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return unitsToMbcs(isEucKr(profile), utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToEucKr(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return unitsToMbcs(isEucKr(profile), utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToEucKr(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return unitsToMbcs(isEucKr(profile), utf16Buffer, utf16Max, utf16Length, profile, eucBuffer, eucMax, eucSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToEucKr(uint32_t utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return scalarToMbcs(isEucKr(profile), utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf32beToEucKr(const uint32_t* utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return utf32ToMbcs(isEucKr(profile), utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUtf32leToEucKr(const uint32_t* utf32, const void* profile, uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize) { + return utf32ToMbcs(isEucKr(profile), utf32, profile, eucBuffer, eucMax, eucSize); +} + +int APS5_VABI sceCesUcs2ToUhc(uint16_t ucs2, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return scalarToMbcs(isUhc(profile), ucs2, profile, uhcBuffer, uhcMax, uhcSize); +} + +int APS5_VABI sceCesUtf8ToUhc(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return unitsToMbcs(isUhc(profile), utf8Buffer, utf8Max, utf8Length, profile, uhcBuffer, uhcMax, uhcSize, decodeUtf8); +} + +int APS5_VABI sceCesUtf16ToUhc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return unitsToMbcs(isUhc(profile), utf16Buffer, utf16Max, utf16Length, profile, uhcBuffer, uhcMax, uhcSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToUhc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return unitsToMbcs(isUhc(profile), utf16Buffer, utf16Max, utf16Length, profile, uhcBuffer, uhcMax, uhcSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToUhc(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return unitsToMbcs(isUhc(profile), utf16Buffer, utf16Max, utf16Length, profile, uhcBuffer, uhcMax, uhcSize, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToUhc(uint32_t utf32, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return scalarToMbcs(isUhc(profile), utf32, profile, uhcBuffer, uhcMax, uhcSize); +} + +int APS5_VABI sceCesUtf32beToUhc(const uint32_t* utf32, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return utf32ToMbcs(isUhc(profile), utf32, profile, uhcBuffer, uhcMax, uhcSize); +} + +int APS5_VABI sceCesUtf32leToUhc(const uint32_t* utf32, const void* profile, uint8_t* uhcBuffer, uint32_t uhcMax, uint32_t* uhcSize) { + return utf32ToMbcs(isUhc(profile), utf32, profile, uhcBuffer, uhcMax, uhcSize); +} + +int APS5_VABI sceCesUcs2ToBig5(uint16_t ucs2, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return scalarToMbcs(isBig5(profile), ucs2, profile, big5Buffer, big5Max, big5Size); +} + +int APS5_VABI sceCesUtf8ToBig5(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return unitsToMbcs(isBig5(profile), utf8Buffer, utf8Max, utf8Length, profile, big5Buffer, big5Max, big5Size, decodeUtf8); +} + +int APS5_VABI sceCesUtf16ToBig5(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return unitsToMbcs(isBig5(profile), utf16Buffer, utf16Max, utf16Length, profile, big5Buffer, big5Max, big5Size, decodeUtf16); +} + +int APS5_VABI sceCesUtf16beToBig5(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return unitsToMbcs(isBig5(profile), utf16Buffer, utf16Max, utf16Length, profile, big5Buffer, big5Max, big5Size, decodeUtf16); +} + +int APS5_VABI sceCesUtf16leToBig5(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return unitsToMbcs(isBig5(profile), utf16Buffer, utf16Max, utf16Length, profile, big5Buffer, big5Max, big5Size, decodeUtf16); +} + +int APS5_VABI sceCesUtf32ToBig5(uint32_t utf32, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return scalarToMbcs(isBig5(profile), utf32, profile, big5Buffer, big5Max, big5Size); +} + +int APS5_VABI sceCesUtf32beToBig5(const uint32_t* utf32, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return utf32ToMbcs(isBig5(profile), utf32, profile, big5Buffer, big5Max, big5Size); +} + +int APS5_VABI sceCesUtf32leToBig5(const uint32_t* utf32, const void* profile, uint8_t* big5Buffer, uint32_t big5Max, uint32_t* big5Size) { + return utf32ToMbcs(isBig5(profile), utf32, profile, big5Buffer, big5Max, big5Size); +} + } diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index 165fe278e8..88adbc635b 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -2,41 +2,6 @@ extern "C" { -int APS5_VABI sceCesBig5ToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesBig5ToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesEucCnToUcs2(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -117,41 +82,6 @@ int APS5_VABI sceCesEucJpUcsProfileSetSbcs(void) { return 0; } -int APS5_VABI sceCesEucKrToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesEucKrToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesEucKrUcsProfileSetSbcs(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -732,11 +662,6 @@ int APS5_VABI sceCesUcs2StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUcs2ToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcs2ToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -747,11 +672,6 @@ int APS5_VABI sceCesUcs2ToEucJp(void) { return 0; } -int APS5_VABI sceCesUcs2ToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcs2ToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -772,11 +692,6 @@ int APS5_VABI sceCesUcs2ToSbc(void) { return 0; } -int APS5_VABI sceCesUcs2ToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitEucCnGb2312(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -847,41 +762,6 @@ int APS5_VABI sceCesUcsToJiscs(void) { return 0; } -int APS5_VABI sceCesUhcToUcs2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf16(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf16be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf16le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf32(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf32be(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUhcToUtf32le(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUhcUcsProfileSetSbcs(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -907,11 +787,6 @@ int APS5_VABI sceCesUtf16StrToMbcsStr(void) { return 0; } -int APS5_VABI sceCesUtf16ToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16ToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -922,11 +797,6 @@ int APS5_VABI sceCesUtf16ToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf16ToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16ToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -947,16 +817,6 @@ int APS5_VABI sceCesUtf16ToSbc(void) { return 0; } -int APS5_VABI sceCesUtf16ToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf16beToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16beToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -967,11 +827,6 @@ int APS5_VABI sceCesUtf16beToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf16beToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16beToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -992,16 +847,6 @@ int APS5_VABI sceCesUtf16beToSbc(void) { return 0; } -int APS5_VABI sceCesUtf16beToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf16leToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16leToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1012,11 +857,6 @@ int APS5_VABI sceCesUtf16leToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf16leToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf16leToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1037,11 +877,6 @@ int APS5_VABI sceCesUtf16leToSbc(void) { return 0; } -int APS5_VABI sceCesUtf16leToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32StrGetIso2022Len(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1062,11 +897,6 @@ int APS5_VABI sceCesUtf32StrToMbcsStr(void) { return 0; } -int APS5_VABI sceCesUtf32ToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32ToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1077,11 +907,6 @@ int APS5_VABI sceCesUtf32ToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf32ToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32ToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1102,16 +927,6 @@ int APS5_VABI sceCesUtf32ToSbc(void) { return 0; } -int APS5_VABI sceCesUtf32ToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf32beToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32beToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1122,11 +937,6 @@ int APS5_VABI sceCesUtf32beToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf32beToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32beToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1147,16 +957,6 @@ int APS5_VABI sceCesUtf32beToSbc(void) { return 0; } -int APS5_VABI sceCesUtf32beToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUtf32leToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32leToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1167,11 +967,6 @@ int APS5_VABI sceCesUtf32leToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf32leToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf32leToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1192,11 +987,6 @@ int APS5_VABI sceCesUtf32leToSbc(void) { return 0; } -int APS5_VABI sceCesUtf32leToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8StrGetIso2022Len(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1207,11 +997,6 @@ int APS5_VABI sceCesUtf8StrToIso2022Str(void) { return 0; } -int APS5_VABI sceCesUtf8ToBig5(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8ToEucCn(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1222,11 +1007,6 @@ int APS5_VABI sceCesUtf8ToEucJp(void) { return 0; } -int APS5_VABI sceCesUtf8ToEucKr(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUtf8ToGb(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1242,9 +1022,4 @@ int APS5_VABI sceCesUtf8ToSJis(void) { return 0; } -int APS5_VABI sceCesUtf8ToUhc(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/tests/GuestCesDbcs.cpp b/core/libs/tests/GuestCesDbcs.cpp index 552f5193e9..753c8f5c3e 100644 --- a/core/libs/tests/GuestCesDbcs.cpp +++ b/core/libs/tests/GuestCesDbcs.cpp @@ -16,9 +16,84 @@ int APS5_VABI sceCesUtf8StrToMbcsStr(void*, const std::uint8_t*, std::uint32_t, int APS5_VABI sceCesMbcsStrToUtf8Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesUcs2StrToMbcsStr(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesMbcsStrToUcs2Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucKrToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesEucKrToUtf16(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucKrToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucKrToUtf16le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucKrToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesEucKrToUtf32be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesEucKrToUtf32le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUhcToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesUhcToUtf16(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf16le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf32be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUhcToUtf32le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesBig5ToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesBig5ToUtf16(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf16le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf32be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesBig5ToUtf32le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUcs2ToEucKr(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8ToEucKr(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16ToEucKr(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16beToEucKr(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16leToEucKr(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32ToEucKr(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32beToEucKr(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32leToEucKr(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUcs2ToUhc(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8ToUhc(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16ToUhc(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16beToUhc(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16leToUhc(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32ToUhc(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32beToUhc(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32leToUhc(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUcs2ToBig5(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf8ToBig5(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16ToBig5(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16beToBig5(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf16leToBig5(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32ToBig5(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32beToBig5(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32leToBig5(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); } using ToUtf8 = int (APS5_VABI*)(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +using ToUcs2 = int (APS5_VABI*)(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +using ToUtf16 = int (APS5_VABI*)(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +using ToUtf32 = int (APS5_VABI*)(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +using FromUcs2 = int (APS5_VABI*)(std::uint16_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +using FromUtf8 = int (APS5_VABI*)(const std::uint8_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +using FromUtf16 = int (APS5_VABI*)(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +using FromUtf32 = int (APS5_VABI*)(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +using FromUtf32Unit = int (APS5_VABI*)(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); + +struct Decoders { + ToUtf8 utf8; + ToUcs2 ucs2; + ToUtf16 utf16[3]; + ToUtf32 utf32[3]; +}; + +struct Encoders { + FromUcs2 ucs2; + FromUtf8 utf8; + FromUtf16 utf16[3]; + FromUtf32 utf32; + FromUtf32Unit utf32Unit[3]; +}; + +constexpr Decoders eucKrDecoders = {sceCesEucKrToUtf8, sceCesEucKrToUcs2, {sceCesEucKrToUtf16, sceCesEucKrToUtf16be, sceCesEucKrToUtf16le}, {sceCesEucKrToUtf32, sceCesEucKrToUtf32be, sceCesEucKrToUtf32le}}; +constexpr Decoders uhcDecoders = {sceCesUhcToUtf8, sceCesUhcToUcs2, {sceCesUhcToUtf16, sceCesUhcToUtf16be, sceCesUhcToUtf16le}, {sceCesUhcToUtf32, sceCesUhcToUtf32be, sceCesUhcToUtf32le}}; +constexpr Decoders big5Decoders = {sceCesBig5ToUtf8, sceCesBig5ToUcs2, {sceCesBig5ToUtf16, sceCesBig5ToUtf16be, sceCesBig5ToUtf16le}, {sceCesBig5ToUtf32, sceCesBig5ToUtf32be, sceCesBig5ToUtf32le}}; +constexpr Encoders eucKrEncoders = {sceCesUcs2ToEucKr, sceCesUtf8ToEucKr, {sceCesUtf16ToEucKr, sceCesUtf16beToEucKr, sceCesUtf16leToEucKr}, sceCesUtf32ToEucKr, {nullptr, sceCesUtf32beToEucKr, sceCesUtf32leToEucKr}}; +constexpr Encoders uhcEncoders = {sceCesUcs2ToUhc, sceCesUtf8ToUhc, {sceCesUtf16ToUhc, sceCesUtf16beToUhc, sceCesUtf16leToUhc}, sceCesUtf32ToUhc, {nullptr, sceCesUtf32beToUhc, sceCesUtf32leToUhc}}; +constexpr Encoders big5Encoders = {sceCesUcs2ToBig5, sceCesUtf8ToBig5, {sceCesUtf16ToBig5, sceCesUtf16beToBig5, sceCesUtf16leToBig5}, sceCesUtf32ToBig5, {nullptr, sceCesUtf32beToBig5, sceCesUtf32leToBig5}}; constexpr int invalidParameter = static_cast(0x805C0001); constexpr int invalidSrcBuffer = static_cast(0x805C0010); @@ -110,6 +185,137 @@ static std::uint32_t RoundTripMismatches(void* context) { return mismatches; } +static std::uint32_t Load(const void* data, std::uint32_t size, int order) { + if (order == 0) { + std::uint16_t unit = 0; + std::uint32_t wide = 0; + if (size == 2) std::memcpy(&unit, data, 2); + else std::memcpy(&wide, data, 4); + return size == 2 ? unit : wide; + } + const auto* bytes = static_cast(data); + std::uint32_t value = 0; + for (std::uint32_t i = 0; i < size; ++i) value |= static_cast(bytes[i]) << (8 * (order == 1 ? size - 1 - i : i)); + return value; +} + +static void Store(void* data, std::uint32_t size, int order, std::uint32_t value) { + if (order == 0) { + const auto unit = static_cast(value); + if (size == 2) std::memcpy(data, &unit, 2); + else std::memcpy(data, &value, 4); + return; + } + auto* bytes = static_cast(data); + for (std::uint32_t i = 0; i < size; ++i) bytes[i] = static_cast(value >> (8 * (order == 1 ? size - 1 - i : i))); +} + +static std::uint32_t Utf8Code(const std::uint8_t* utf8, std::uint32_t length) { + if (length == 1) return utf8[0]; + if (length == 2) return (utf8[0] & 0x1Fu) << 6 | (utf8[1] & 0x3Fu); + return (utf8[0] & 0x0Fu) << 12 | (utf8[1] & 0x3Fu) << 6 | (utf8[2] & 0x3Fu); +} + +static std::uint32_t Utf8Bytes(std::uint32_t code, std::uint8_t* utf8) { + if (code < 0x80) { + utf8[0] = static_cast(code); + return 1; + } + const std::uint32_t length = code < 0x800 ? 2 : code < 0x10000 ? 3 : 4; + for (std::uint32_t i = length - 1; i > 0; --i) { + utf8[i] = static_cast(0x80 | (code & 0x3F)); + code >>= 6; + } + utf8[0] = static_cast((length == 2 ? 0xC0 : length == 3 ? 0xE0 : 0xF0) | code); + return length; +} + +static void RequireDecoders(const Decoders& decoders, const void* profile) { + for (std::uint32_t lead = 0; lead < 256; ++lead) { + for (std::uint32_t trail = 0; trail < 256; ++trail) { + const std::uint8_t source[2] = {static_cast(lead), static_cast(trail)}; + std::uint8_t utf8[4] = {}; + std::uint32_t consumed = 0; + std::uint32_t produced = 0; + const int expected = decoders.utf8(profile, source, sizeof(source), &consumed, utf8, sizeof(utf8), &produced); + const std::uint32_t code = expected == 0 ? Utf8Code(utf8, produced) : 0; + std::uint16_t ucs2 = 0x5555; + std::uint32_t read = 77; + Require(decoders.ucs2(profile, source, sizeof(source), &read, &ucs2) == expected); + Require(expected == 0 ? read == consumed && ucs2 == code : read == 77 && ucs2 == 0x5555); + for (int order = 0; order < 3; ++order) { + std::uint16_t utf16 = 0x5555; + std::uint32_t units = 77; + read = 77; + Require(decoders.utf16[order](profile, source, sizeof(source), &read, &utf16, 1, &units) == expected); + Require(expected == 0 ? read == consumed && units == 1 && Load(&utf16, 2, order) == code : read == 77 && units == 77 && utf16 == 0x5555); + std::uint32_t utf32 = 0x55555555; + read = 77; + Require(decoders.utf32[order](profile, source, sizeof(source), &read, &utf32) == expected); + Require(expected == 0 ? read == consumed && Load(&utf32, 4, order) == code : read == 77 && utf32 == 0x55555555); + } + } + } +} + +struct Encoded { + std::uint8_t bytes[2] = {0x55, 0x55}; + std::uint32_t size = 77; + std::uint32_t read = 77; +}; + +static int Encode(const Encoders& encoders, int form, std::uint32_t code, const void* profile, Encoded& out) { + std::uint8_t utf8[4] = {}; + std::uint16_t utf16[2] = {}; + std::uint32_t utf32 = 0; + if (form == 0) return encoders.ucs2(static_cast(code), profile, out.bytes, sizeof(out.bytes), &out.size); + if (form == 1) return encoders.utf8(utf8, Utf8Bytes(code, utf8), &out.read, profile, out.bytes, sizeof(out.bytes), &out.size); + if (form <= 4) { + const int order = form - 2; + if (code < 0x10000) Store(utf16, 2, order, code); + else { + Store(utf16, 2, order, 0xD800 | (code - 0x10000) >> 10); + Store(utf16 + 1, 2, order, 0xDC00 | (code & 0x3FF)); + } + return encoders.utf16[order](utf16, code < 0x10000 ? 1 : 2, &out.read, profile, out.bytes, sizeof(out.bytes), &out.size); + } + if (form == 5) return encoders.utf32(code, profile, out.bytes, sizeof(out.bytes), &out.size); + Store(&utf32, 4, form - 5, code); + return encoders.utf32Unit[form - 5](&utf32, profile, out.bytes, sizeof(out.bytes), &out.size); +} + +static std::uint32_t SourceUnits(int form, std::uint32_t code) { + if (form == 1) return code < 0x80 ? 1 : code < 0x800 ? 2 : code < 0x10000 ? 3 : 4; + if (form >= 2 && form <= 4) return code < 0x10000 ? 1 : 2; + return 77; +} + +static void RequireEncode(const Encoders& encoders, const void* profile, std::uint32_t code, int expected, const std::uint8_t* bytes, std::uint32_t size) { + for (int form = 0; form < 8; ++form) { + if (form == 0 && code > 0xFFFF) continue; + Encoded out; + Require(Encode(encoders, form, code, profile, out) == expected); + if (expected == 0) Require(out.read == SourceUnits(form, code) && out.size == size && std::memcmp(out.bytes, bytes, size) == 0); + else Require(out.read == 77 && out.size == 77 && out.bytes[0] == 0x55 && out.bytes[1] == 0x55); + } +} + +static void RequireEncoders(const Encoders& encoders, const void* profile, void* context) { + Require(sceCesMbcsUcsContextInit(context, profile) == 0); + for (std::uint32_t code = 1; code < 0x10000; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + const auto ucs2 = static_cast(code); + std::uint8_t bytes[2] = {}; + std::uint32_t read = 0; + std::uint32_t written = 0; + const int expected = sceCesUcs2StrToMbcsStr(context, &ucs2, 1, &read, bytes, sizeof(bytes), &written); + RequireEncode(encoders, profile, code, expected, bytes, written); + } + RequireEncode(encoders, profile, 0, 0, Bytes("\0"), 1); + RequireEncode(encoders, profile, 0x20000, unassignedCode, nullptr, 0); + RequireEncode(encoders, profile, 0x10FFFF, unassignedCode, nullptr, 0); +} + int main() { alignas(8) std::uint8_t eucKrSheet[64] = {}; alignas(8) std::uint8_t uhcSheet[64] = {}; @@ -175,5 +381,87 @@ int main() { RequireString(context, "\xF9\xE9\xF9\xEB\xF9\xEA\xA2\xA1\xA2\xA3\xA2\xA2", "\xE2\x95\x9E\xE2\x95\xA1\xE2\x95\xAA\xE2\x95\xAE\xE2\x95\xAF\xE2\x95\xB0"); Require(Utf8ToMbcs(context, "\xEA\xB0\x80") == unassignedCode); Require(RoundTripMismatches(context) == 10); + + RequireDecoders(eucKrDecoders, eucKr); + RequireDecoders(uhcDecoders, uhc); + RequireDecoders(big5Decoders, big5); + RequireEncoders(eucKrEncoders, eucKr, context); + RequireEncoders(uhcEncoders, uhc, context); + RequireEncoders(big5Encoders, big5, context); + + const std::uint8_t* ga = Bytes("\xB0\xA1"); + std::uint16_t unit = 0; + std::uint32_t wide = 0; + std::uint32_t read = 0; + std::uint32_t units = 0; + Require(sceCesEucKrToUtf16be(eucKr, ga, 2, &read, &unit, 1, &units) == 0 && read == 2 && units == 1 && std::memcmp(&unit, "\xAC\x00", 2) == 0); + Require(sceCesEucKrToUtf16le(eucKr, ga, 2, &read, &unit, 1, &units) == 0 && std::memcmp(&unit, "\x00\xAC", 2) == 0); + Require(sceCesEucKrToUtf16(eucKr, ga, 2, nullptr, &unit, 1, nullptr) == 0 && unit == 0xAC00); + Require(sceCesEucKrToUtf32be(eucKr, ga, 2, &read, &wide) == 0 && read == 2 && std::memcmp(&wide, "\x00\x00\xAC\x00", 4) == 0); + Require(sceCesEucKrToUtf32le(eucKr, ga, 2, &read, &wide) == 0 && std::memcmp(&wide, "\x00\xAC\x00\x00", 4) == 0); + Require(sceCesEucKrToUtf32(eucKr, ga, 2, nullptr, &wide) == 0 && wide == 0xAC00); + Require(sceCesEucKrToUcs2(eucKr, Bytes("\0"), 1, &read, &unit) == 0 && read == 1 && unit == 0); + Require(sceCesUhcToUcs2(uhc, Bytes("\x8C\x63"), 2, nullptr, &unit) == 0 && unit == 0xB620); + Require(sceCesBig5ToUtf32le(big5, Bytes("\xA4\xA4"), 2, &read, &wide) == 0 && std::memcmp(&wide, "\x2D\x4E\x00\x00", 4) == 0); + + Require(sceCesEucKrToUtf16(uhc, nullptr, 0, nullptr, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesEucKrToUtf16(eucKr, nullptr, 0, nullptr, nullptr, 0, nullptr) == invalidSrcBuffer); + Require(sceCesEucKrToUtf16(eucKr, ga, 0, nullptr, nullptr, 0, nullptr) == srcBufferEnd); + Require(sceCesEucKrToUtf16(eucKr, ga, 2, nullptr, nullptr, 0, nullptr) == invalidDstBuffer); + Require(sceCesEucKrToUtf16(eucKr, Bytes("\x8C\x63"), 2, nullptr, &unit, 0, nullptr) == invalidEncode); + unit = 0x5555; + read = 77; + units = 77; + Require(sceCesEucKrToUtf16(eucKr, ga, 2, &read, &unit, 0, &units) == dstBufferEnd && unit == 0x5555 && read == 77 && units == 77); + Require(sceCesUhcToUcs2(uhc, ga, 2, nullptr, nullptr) == invalidDstBuffer); + Require(sceCesBig5ToUtf32(big5, Bytes("\xA4\xA4"), 1, nullptr, &wide) == srcBufferEnd); + Require(sceCesBig5ToUtf32be(eucJp, Bytes("\xA4\xA4"), 2, nullptr, &wide) == invalidParameter); + Require(sceCesBig5ToUtf32le(big5, Bytes("\xA4\xA4"), 2, nullptr, nullptr) == invalidDstBuffer); + + RequireEncode(eucKrEncoders, eucKr, 0x20AC, 0, Bytes("\xA2\xE6"), 2); + RequireEncode(eucKrEncoders, eucKr, 0xB620, unassignedCode, nullptr, 0); + RequireEncode(uhcEncoders, uhc, 0xB620, 0, Bytes("\x8C\x63"), 2); + RequireEncode(big5Encoders, big5, 0x2550, 0, Bytes("\xF9\xF9"), 2); + RequireEncode(big5Encoders, big5, 0x5341, 0, Bytes("\xA4\x51"), 2); + RequireEncode(big5Encoders, big5, 0xAC00, unassignedCode, nullptr, 0); + RequireEncode(eucKrEncoders, uhc, 0xAC00, invalidParameter, nullptr, 0); + RequireEncode(uhcEncoders, nullptr, 0xAC00, invalidParameter, nullptr, 0); + RequireEncode(big5Encoders, eucJp, 0x4E2D, invalidParameter, nullptr, 0); + + std::uint8_t out[2] = {0x55, 0x55}; + std::uint32_t size = 77; + read = 77; + const std::uint32_t big = 0x0000AC00; + Require(sceCesUcs2ToEucKr(0xAC00, uhc, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesUcs2ToEucKr(0xD800, eucKr, nullptr, 0, nullptr) == invalidDstBuffer); + Require(sceCesUcs2ToEucKr(0xD800, eucKr, out, 2, &size) == invalidEncode); + Require(sceCesUtf32ToUhc(0x110000, uhc, out, 2, &size) == invalidEncode); + Require(sceCesUtf32ToUhc(0xDFFF, uhc, out, 2, &size) == invalidEncode); + Require(sceCesUtf32beToBig5(nullptr, eucKr, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesUtf32beToBig5(nullptr, big5, nullptr, 0, nullptr) == invalidSrcBuffer); + Require(sceCesUtf32leToBig5(&big, big5, nullptr, 0, nullptr) == invalidDstBuffer); + Require(sceCesUtf8ToEucKr(nullptr, 0, nullptr, uhc, nullptr, 0, nullptr) == invalidParameter); + Require(sceCesUtf8ToEucKr(nullptr, 0, nullptr, eucKr, nullptr, 0, nullptr) == invalidSrcBuffer); + Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 0, nullptr, eucKr, nullptr, 0, nullptr) == srcBufferEnd); + Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 3, nullptr, eucKr, nullptr, 0, nullptr) == invalidDstBuffer); + Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 2, &read, eucKr, out, 2, &size) == srcBufferEnd); + Require(sceCesUtf8ToEucKr(Bytes("\xC0\x80"), 2, &read, eucKr, out, 2, &size) == invalidEncode); + Require(sceCesUtf8ToEucKr(Bytes("\xEA\xB0\x80"), 3, &read, eucKr, out, 1, &size) == dstBufferEnd); + const std::uint16_t lowSurrogate[1] = {0xDC00}; + const std::uint16_t highSurrogate[2] = {0xD800, 0x0041}; + Require(sceCesUtf16ToUhc(lowSurrogate, 1, &read, uhc, out, 2, &size) == invalidEncode); + Require(sceCesUtf16ToUhc(highSurrogate, 1, &read, uhc, out, 2, &size) == srcBufferEnd); + Require(sceCesUtf16ToUhc(highSurrogate, 2, &read, uhc, out, 2, &size) == invalidEncode); + Require(out[0] == 0x55 && out[1] == 0x55 && size == 77 && read == 77); + + std::memcpy(&unit, "\xAC\x00", 2); + Require(sceCesUtf16beToEucKr(&unit, 1, &read, eucKr, out, 2, &size) == 0 && read == 1 && size == 2 && std::memcmp(out, "\xB0\xA1", 2) == 0); + std::memcpy(&unit, "\x00\xAC", 2); + Require(sceCesUtf16leToUhc(&unit, 1, nullptr, uhc, out, 2, nullptr) == 0 && std::memcmp(out, "\xB0\xA1", 2) == 0); + std::memcpy(&wide, "\x00\x00\x4E\x2D", 4); + Require(sceCesUtf32beToBig5(&wide, big5, out, 2, &size) == 0 && size == 2 && std::memcmp(out, "\xA4\xA4", 2) == 0); + std::memcpy(&wide, "\x2D\x4E\x00\x00", 4); + Require(sceCesUtf32leToBig5(&wide, big5, out, 2, nullptr) == 0 && std::memcmp(out, "\xA4\xA4", 2) == 0); + Require(sceCesUcs2ToEucKr(0, eucKr, out, 1, &size) == 0 && size == 1 && out[0] == 0); return 0; } diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 14cef5e67e..0cc0674e2e 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -11,5 +11,5 @@ - [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run - [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 249 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. -- [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 single-byte profile (`sceCesEucKrUcsProfileSetSbcs`) is not implemented. The user-defined areas, which Windows maps to the Private Use Area, answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as Windows code page 950 does. The `*ToUtf8` calls convert one character and leave their outputs unchanged on failure +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 204 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 profile (`sceCes*UcsProfileSetSbcs`) is not implemented. User-defined areas (Private Use Area on Windows) answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The scalar calls to and from UCS-2 and UTF-8/16/32 convert one character as the Unicode scalar conversions do From f2bc1195f863f148ada0822a3d841a9f9c481f5b Mon Sep 17 00:00:00 2001 From: Retr0 Date: Wed, 21 Oct 2026 15:22:38 +0300 Subject: [PATCH 125/318] fix(libkernel): reject socket addresses longer than sockaddr_in or sockaddr_in6 with EINVAL FreeBSD 11.4 getsockaddr sets sa_len to the length argument, and tcp_usr_bind, tcp_usr_connect, in_pcbbind_setup, in_pcbconnect_setup, in6_pcbbind and in6_pcbladdr answer EINVAL when it is not the size of the socket family's address. udp6_bind and udp6_connect pass an IPv4-mapped address to the IPv4 code without that check when IPV6_V6ONLY is clear, and udp6_send always checks it. bind and connect on AF_INET and AF_INET6 sockets, and sendto on AF_INET6 datagram sockets, now fail a longer address with EINVAL. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 12 ++++ core/libs/tests/GuestSocketAddressLength.cpp | 64 ++++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index fbf308a2e4..66f967656d 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -290,6 +290,15 @@ bool Address(const void* input, std::uint32_t length, sockaddr_storage& native, Fail(47); return false; } +int LongAddressError(const Socket& socket, const void* address, std::uint32_t length) { + if ((socket.family != 2 && socket.family != 28) || length <= (socket.family == 2 ? 16u : 28u)) return 0; + constexpr unsigned char mapped[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}; + if (socket.family != 28 || socket.type != 2 || std::memcmp(static_cast(address) + 8, mapped, sizeof(mapped)) != 0) return 22; + int ipv6Only = 0; + socklen_t size = sizeof(ipv6Only); + if (::getsockopt(socket.value, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&ipv6Only), &size)) return NativeError(); + return ipv6Only ? 22 : 0; +} std::uint16_t& PortOf(sockaddr_storage& address) { return address.ss_family == AF_INET ? reinterpret_cast(address).sin_port : reinterpret_cast(address).sin6_port; } @@ -1190,6 +1199,7 @@ int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint if (socket->type == 1) RefuseClosedLoopbackAtOnce_nid_no_patch(socket->value, reinterpret_cast(&native)); #endif if (socket->type == 1 && Listening(*socket)) return Fail(45); + if (const int error = LongAddressError(*socket, address, length)) return Fail(error); if (ZeroPortConnect(*socket, address, length)) return -1; if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); { @@ -1268,6 +1278,7 @@ int APS5_VABI bind_nid_postfix(int descriptor, const void* address, std::uint32_ sockaddr_storage native{}; socklen_t size; if (!Address(address, length, native, size)) return -1; + if (const int error = LongAddressError(*socket, address, length)) return Fail(error); return ::bind(socket->value, reinterpret_cast(&native), size) ? Fail(NativeError()) : 0; } int APS5_VABI shutdown_nid_postfix(int descriptor, int how) { @@ -1351,6 +1362,7 @@ std::int64_t APS5_VABI sendto_nid_postfix(int descriptor, const void* buffer, st sockaddr_storage native{}; socklen_t size; if (!Address(address, addressLength, native, size)) return -1; + if (socket->family == 28 && socket->type == 2 && addressLength > 28) return Fail(22); if (native.ss_family != (socket->family == 2 ? AF_INET : AF_INET6)) return Fail(47); result = static_cast(::sendto(socket->value, static_cast(buffer), static_cast(length), SocketSendFlags(*socket, flags), reinterpret_cast(&native), size)); diff --git a/core/libs/tests/GuestSocketAddressLength.cpp b/core/libs/tests/GuestSocketAddressLength.cpp index 930a8626d6..e0e4833d0d 100644 --- a/core/libs/tests/GuestSocketAddressLength.cpp +++ b/core/libs/tests/GuestSocketAddressLength.cpp @@ -9,6 +9,9 @@ extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI connect_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI listen_nid_postfix(int, int); +int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*); +int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); std::int64_t APS5_VABI sendto_nid_postfix(int, const void*, std::uint64_t, int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); int* APS5_VABI __error_nid_postfix(); @@ -64,10 +67,71 @@ void FullAddressesStillWork() { Require(close_nid_postfix(stream) == 0, "close failed"); } +std::array PaddedLoopback(int family) { + std::array address{}; + if (family == 2) { + const auto v4 = Loopback(); + std::memcpy(address.data(), v4.data(), v4.size()); + } else { + address[0] = 28; + address[1] = 28; + address[23] = 1; + } + return address; +} + +void LongAddressesAreInvalid(int family, int type) { + const std::uint32_t size = family == 2 ? 16 : 28; + auto address = PaddedLoopback(family); + const int descriptor = socket_nid_postfix(family, type, 0); + const int listener = type == Stream ? socket_nid_postfix(family, Stream, 0) : descriptor; + Require(descriptor >= 0 && listener >= 0, "socket failed"); + for (std::uint32_t length = size + 1; length <= address.size(); ++length) { + Require(Fails(bind_nid_postfix(descriptor, address.data(), length), ErrorInvalid), "bind with an address longer than sockaddr_in or sockaddr_in6 must fail with EINVAL (22)"); + } + Require(bind_nid_postfix(listener, address.data(), size) == 0, "bind with an address of exactly the size of sockaddr_in or sockaddr_in6 failed"); + if (type == Stream) Require(listen_nid_postfix(listener, 1) == 0, "listen failed"); + std::uint32_t named = size; + Require(getsockname_nid_postfix(listener, address.data(), &named) == 0 && named == size, "getsockname failed"); + for (std::uint32_t length = size + 1; length <= address.size(); ++length) { + Require(Fails(connect_nid_postfix(descriptor, address.data(), length), ErrorInvalid), "connect with an address longer than sockaddr_in or sockaddr_in6 must fail with EINVAL (22)"); + } + if (family == 28 && type == Datagram) { + for (std::uint32_t length = size + 1; length <= address.size(); ++length) { + Require(Fails(sendto_nid_postfix(descriptor, "x", 1, 0, address.data(), length), ErrorInvalid), "sendto on an AF_INET6 datagram socket with an address longer than sockaddr_in6 must fail with EINVAL (22)"); + } + Require(sendto_nid_postfix(descriptor, "x", 1, 0, address.data(), size) == 1, "sendto with an address of exactly the size of sockaddr_in6 failed"); + } + Require(connect_nid_postfix(descriptor, address.data(), size) == 0, "connect with an address of exactly the size of sockaddr_in or sockaddr_in6 failed"); + Require(close_nid_postfix(descriptor) == 0, "close failed"); + if (listener != descriptor) Require(close_nid_postfix(listener) == 0, "close failed"); +} + +void LongMappedAddressesBindOnlyDualStackDatagramSockets() { + const int datagram = socket_nid_postfix(28, Datagram, 0); + Require(datagram >= 0, "socket failed"); + std::array mapped{29, 28}; + mapped[18] = 0xff; + mapped[19] = 0xff; + mapped[20] = 127; + mapped[23] = 1; + Require(Fails(bind_nid_postfix(datagram, mapped.data(), mapped.size()), ErrorInvalid), "bind of a long IPv4-mapped address with IPV6_V6ONLY set must fail with EINVAL (22)"); + const int off = 0; + Require(setsockopt_nid_postfix(datagram, 41, 27, &off, sizeof(off)) == 0, "clearing IPV6_V6ONLY failed"); + Require(bind_nid_postfix(datagram, mapped.data(), mapped.size()) == 0, "udp6_bind passes a long IPv4-mapped address to in_pcbbind without IPV6_V6ONLY, so bind must succeed"); + Require(Fails(sendto_nid_postfix(datagram, "x", 1, 0, mapped.data(), mapped.size()), ErrorInvalid), "udp6_send checks the length before the IPv4-mapped case, so sendto must fail with EINVAL (22)"); + Require(close_nid_postfix(datagram) == 0, "close failed"); +} + } int main() { ShortAddressesAreInvalid(); FullAddressesStillWork(); + LongAddressesAreInvalid(2, Stream); + LongAddressesAreInvalid(2, Datagram); + LongAddressesAreInvalid(28, Stream); + LongAddressesAreInvalid(28, Datagram); + LongMappedAddressesBindOnlyDualStackDatagramSockets(); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index af8cab87d7..b66c342ff4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -189,7 +189,7 @@ - [recvmsg](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) loses received stream data on an output copy fault: outputs are checked after the host receive. FreeBSD 11.4 leaves unread stream data queued, though it also drops a datagram on a copy fault ([`soreceive_dgram`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L2308-L2312), [`soreceive_generic`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L1758-L1774)). A Linux `process_vm_readv` or `process_vm_writev` failure other than `EFAULT` throws - [MSG_DONTWAIT](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) in `send`, `sendto`, `sendmsg`, `recv`, `recvfrom` and `recvmsg` makes that one call non-blocking through the host's `MSG_DONTWAIT`, as FreeBSD 11.4's `soreceive_generic` returns `EWOULDBLOCK` for it (`sys/kern/uipc_socket.c`). Winsock has no per-call non-blocking flag, so on Windows the call first asks `WSAPoll` with a zero timeout whether the socket is ready and fails with `EAGAIN` when it is not. The check and the transfer are two host calls: a second thread that takes the data in between can leave a `recv` blocked, and a `send` of more bytes than the socket buffer holds can still block until it has sent them, where FreeBSD's `sosend_generic` returns a partial count - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). -- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address length as FreeBSD 11.4's [`getsockaddr`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_syscalls.c) and the `sa_len` test of `tcp_usr_connect` and `in_pcbconnect` do: below 2 or an `AF_INET` address below 16 or an `AF_INET6` address below 28 bytes fails with `EINVAL`, above 255 with `ENAMETOOLONG`. An address longer than `sockaddr_in` or `sockaddr_in6` is still accepted where FreeBSD answers `EINVAL`, and an address of another family still fails with `EAFNOSUPPORT`, as the console's behaviour for these is not known. None of this was measured on a console. +- [bind, connect, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) check a socket address length as FreeBSD 11.4's [`getsockaddr`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_syscalls.c) and the `sa_len` test of `tcp_usr_connect` and `in_pcbconnect` do: below 2 or an `AF_INET` address below 16 or an `AF_INET6` address below 28 bytes fails with `EINVAL`, above 255 with `ENAMETOOLONG`. `sendto` does not check an address longer than `sockaddr_in` on an `AF_INET` datagram socket, where `udp_output` answers `EINVAL` unless the socket has a local address and port and the destination is neither `INADDR_ANY` nor `INADDR_BROADCAST`, nor on an unconnected stream socket, which FreeBSD connects implicitly through `tcp_connect` and its length test. An address of another family still fails with `EAFNOSUPPORT`, as the console's behaviour for it is not known. None of this was measured on a console. - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux unless the socket has `SO_NOSIGPIPE` set, and the guest cannot ignore it: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; nothing is raised on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` - [fstat on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) fills the fields FreeBSD 11's soo_stat and pru_sense_null fill and zeroes the rest: S_IFSOCK, the read bits while the receive side is open or data is queued, the write bits while the send side is open, st_size and st_blksize. Only this process's shutdown closes a side here; a peer's shutdown, close or reset does not clear the bits as FreeBSD's socantrcvmore and socantsendmore do. st_size is the host's FIONREAD, which for a Linux datagram socket is the size of the next datagram where FreeBSD counts every queued byte, and st_blksize is the host's SO_SNDBUF, which Linux reports as twice the size that was set. A socketpair end reports st_dev and st_ino 0, where FreeBSD's uipc_sense reports NODEV and a per-socket number From b7794963150cbd894234aa2d1ad22133f3c60318 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 09:56:48 -0300 Subject: [PATCH 126/318] fix(libkernel): match event flag attributes and error results to PS5 Measured on firmware 13.60: create accepts attribute bits 0x1, 0x2, 0x10, 0x20 and 0x100 and rejects 0x1|0x2; wait and poll ignore mode bits outside the match and clear fields; EPERM, ECANCELED and EACCES write 0 to the result pattern. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/EventFlag/src/EventFlag.cpp | 21 +++++++--- core/libs/tests/GuestKernelErrors.cpp | 39 +++++++++++++++++-- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index a6403ce990..e9d51f67c1 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -27,8 +27,11 @@ static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; static constexpr int SCE_KERNEL_ERROR_ECANCELED = 0x80020055; +static constexpr uint32_t EVF_ATTR_TH_FIFO = 0x01; +static constexpr uint32_t EVF_ATTR_TH_PRIO = 0x02; static constexpr uint32_t EVF_ATTR_SINGLE = 0x10; static constexpr uint32_t EVF_ATTR_MULTI = 0x20; +static constexpr uint32_t EVF_ATTR_UNKNOWN_0x100 = 0x100; static constexpr uint32_t EVF_WAITMODE_AND = 0x01; static constexpr uint32_t EVF_WAITMODE_OR = 0x02; static constexpr uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; @@ -110,7 +113,6 @@ static bool TraceSync() { static bool IsValidWaitMode(uint32_t waitMode) { const uint32_t match = waitMode & (EVF_WAITMODE_AND | EVF_WAITMODE_OR); const uint32_t clear = waitMode & (EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT); - if ((waitMode & ~(EVF_WAITMODE_AND | EVF_WAITMODE_OR | EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT)) != 0) return false; return (match == EVF_WAITMODE_AND || match == EVF_WAITMODE_OR) && clear != (EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT); } @@ -130,7 +132,9 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui (void)param; if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; if ((attr & EVF_ATTR_SINGLE) && (attr & EVF_ATTR_MULTI)) return SCE_KERNEL_ERROR_EINVAL; - if ((attr & ~0x33u) != 0) return SCE_KERNEL_ERROR_EINVAL; + if ((attr & EVF_ATTR_TH_FIFO) && (attr & EVF_ATTR_TH_PRIO)) return SCE_KERNEL_ERROR_EINVAL; + if ((attr & ~(EVF_ATTR_TH_FIFO | EVF_ATTR_TH_PRIO | EVF_ATTR_SINGLE | EVF_ATTR_MULTI | EVF_ATTR_UNKNOWN_0x100)) != 0) return SCE_KERNEL_ERROR_EINVAL; + if (attr & EVF_ATTR_UNKNOWN_0x100) NotImplemented_nid_no_patch("sceKernelCreateEventFlag attribute 0x100"); std::shared_ptr flag; try { flag = std::make_shared(); @@ -226,7 +230,10 @@ int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u if (bit_pattern == 0 || !IsValidWaitMode(wait_mode)) return SCE_KERNEL_ERROR_EINVAL; std::lock_guard lock(ef->_mutex); if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - if (!ef->_multipleWaiters && ef->_waiters != 0) return SCE_KERNEL_ERROR_EPERM; + if (!ef->_multipleWaiters && ef->_waiters != 0) { + if (result_pat) *result_pat = 0; + return SCE_KERNEL_ERROR_EPERM; + } if (!IsSatisfied(ef->_pattern, bit_pattern, wait_mode)) { if (result_pat) *result_pat = ef->_pattern; return SCE_KERNEL_ERROR_EBUSY; @@ -242,7 +249,10 @@ int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u if (bit_pattern == 0 || !IsValidWaitMode(wait_mode)) return SCE_KERNEL_ERROR_EINVAL; std::unique_lock lock(ef->_mutex); if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - if (!ef->_multipleWaiters && ef->_waiters != 0) return SCE_KERNEL_ERROR_EPERM; + if (!ef->_multipleWaiters && ef->_waiters != 0) { + if (result_pat) *result_pat = 0; + return SCE_KERNEL_ERROR_EPERM; + } if (IsSatisfied(ef->_pattern, bit_pattern, wait_mode)) { Consume(ef, bit_pattern, wait_mode, result_pat); return SCE_OK; @@ -275,10 +285,11 @@ int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u } KernelTraceWait_nid_postfix("evf", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); if (ef->_deleted) { + if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_EACCES; } if (ef->_cancelGeneration != generation) { - if (result_pat) *result_pat = ef->_pattern; + if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_ECANCELED; } if (timedOut && !waiter.satisfied) { diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 1932625356..61a1d7ae0c 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -198,6 +198,13 @@ int main() { Require(sceKernelDeleteEventFlag(ef) == SCE_OK); Require(sceKernelCreateEventFlag(&ef, "test", EVF_ATTR_SINGLE | EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelCreateEventFlag(&ef, "test", EVF_ATTR_MULTI | 0x1000u, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + for (const std::uint32_t attr : {0x03u, 0x13u, 0x23u, 0x103u, 0x130u, 0x04u, 0x08u, 0x40u, 0x80u, 0x200u, 0x80000000u, 0xffffffffu}) { + Require(sceKernelCreateEventFlag(&ef, "test_bad_attr", attr, 0, nullptr) == SCE_KERNEL_ERROR_EINVAL); + } + for (const std::uint32_t attr : {0x02u, 0x11u, 0x12u, 0x21u, 0x22u}) { + Require(sceKernelCreateEventFlag(&ef, "test_attr", attr, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + } Require(sceKernelDeleteEventFlag(nullptr) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelSetEventFlag(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelClearEventFlag(nullptr, 1) == SCE_KERNEL_ERROR_ESRCH); @@ -224,6 +231,16 @@ int main() { Require(sceKernelPollEventFlag(ef, 1, 0, &pattern) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelPollEventFlag(ef, 1, EVF_WAITMODE_AND | EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelPollEventFlag(ef, 1, EVF_WAITMODE_AND | EVF_WAITMODE_CLEAR_ALL | EVF_WAITMODE_CLEAR_PAT, &pattern) == SCE_KERNEL_ERROR_EINVAL); + for (const std::uint32_t mode : {0x04u, 0x40u, 0x100u, 0x80000000u, 0x103u}) { + pattern = 0xdeadbeef; + Require(sceKernelPollEventFlag(ef, 1, mode, &pattern) == SCE_KERNEL_ERROR_EINVAL && pattern == 0xdeadbeef); + } + for (const std::uint32_t mode : {EVF_WAITMODE_OR | 0x04u, EVF_WAITMODE_OR | 0x08u, EVF_WAITMODE_AND | 0x40u, EVF_WAITMODE_AND | 0x100u, EVF_WAITMODE_OR | 0x80000000u, + EVF_WAITMODE_OR | 0xffffff00u}) { + Require(sceKernelPollEventFlag(ef, 0x10, mode, &pattern) == SCE_OK && pattern == 0x15); + } + Require(sceKernelPollEventFlag(ef, 0x10, EVF_WAITMODE_OR | EVF_WAITMODE_CLEAR_PAT | 0x100u, &pattern) == SCE_OK && pattern == 0x15); + Require(sceKernelSetEventFlag(ef, 0x10) == SCE_OK); Require(sceKernelPollEventFlag(ef, 0x20, EVF_WAITMODE_OR, &pattern) == SCE_KERNEL_ERROR_EBUSY); Require(pattern == 0x15); @@ -261,7 +278,22 @@ int main() { Require(sceKernelDeleteEventFlag(ef) == SCE_OK); - Require(sceKernelCreateEventFlag(&ef, "single", EVF_ATTR_SINGLE | 0x02u, 0, nullptr) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "single", EVF_ATTR_SINGLE | 0x02u, 0x04, nullptr) == SCE_OK); + std::atomic singleRes{-1}; + std::uint64_t singlePat = 0xdeadbeef; + std::thread singleWaiter([&] { + singleRes = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND, &singlePat, nullptr); + }); + WaitForEventFlagWaiters(ef, 1); + pattern = 0xdeadbeef; + Require(sceKernelPollEventFlag(ef, 0x04, EVF_WAITMODE_AND, &pattern) == SCE_KERNEL_ERROR_EPERM && pattern == 0); + pattern = 0xdeadbeef; + Require(sceKernelWaitEventFlag(ef, 0x04, EVF_WAITMODE_AND, &pattern, nullptr) == SCE_KERNEL_ERROR_EPERM && pattern == 0); + waitThreads = -1; + Require(sceKernelCancelEventFlag(ef, 0x08, &waitThreads) == SCE_OK && waitThreads == 1); + singleWaiter.join(); + Require(singleRes == SCE_KERNEL_ERROR_ECANCELED && singlePat == 0); + Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_AND, &pattern) == SCE_OK && pattern == 0x08); Require(sceKernelDeleteEventFlag(ef) == SCE_OK); Require(sceKernelCreateEventFlag(&ef, "imm_sat", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); @@ -323,8 +355,9 @@ int main() { Require(sceKernelCreateEventFlag(&ef, "del_waiters", EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); std::atomic delRes1{-1}; std::atomic delRes2{-1}; + std::uint64_t delPat = 0xdeadbeef; std::thread delT1([&] { - delRes1 = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND, nullptr, nullptr); + delRes1 = sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND, &delPat, nullptr); }); std::thread delT2([&] { delRes2 = sceKernelWaitEventFlag(ef, 0x02, EVF_WAITMODE_AND, nullptr, nullptr); @@ -333,7 +366,7 @@ int main() { Require(sceKernelDeleteEventFlag(ef) == SCE_OK); delT1.join(); delT2.join(); - Require(delRes1 == SCE_KERNEL_ERROR_EACCES); + Require(delRes1 == SCE_KERNEL_ERROR_EACCES && delPat == 0); Require(delRes2 == SCE_KERNEL_ERROR_EACCES); for (int iter = 0; iter < 50; ++iter) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index af8cab87d7..a8dc3a9fc1 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -59,7 +59,7 @@ - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console -- [sceKernelSetEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - multi-waiter satisfaction follows fpPS4 in-order evaluation: registered waiters are evaluated in FIFO order and each satisfied waiter's clear mode is applied immediately to the pattern before evaluating subsequent waiters; this ordering is unverified on PS5 console hardware +- [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - attribute bit 0x100 is accepted by firmware 13.60 alongside 0x1, 0x2, 0x10 and 0x20, but what it changes is unknown, so a flag created with it calls `NotImplemented_nid_no_patch`. - [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked From 3e6c6bd71f167ca87e663139b05f5a7a54db931d Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 09:57:32 -0300 Subject: [PATCH 127/318] test(libkernel): cover cancelling two event flag waiters Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestKernelErrors.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index 61a1d7ae0c..efb2403c29 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -296,6 +296,27 @@ int main() { Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_AND, &pattern) == SCE_OK && pattern == 0x08); Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "multi_cancel", EVF_ATTR_MULTI, 0x01, nullptr) == SCE_OK); + std::atomic cancelRes1{-1}; + std::atomic cancelRes2{-1}; + std::uint64_t cancelPat1 = 0xdeadbeef; + std::uint64_t cancelPat2 = 0xdeadbeef; + std::thread cancelT1([&] { + cancelRes1 = sceKernelWaitEventFlag(ef, 0x02, EVF_WAITMODE_AND, &cancelPat1, nullptr); + }); + std::thread cancelT2([&] { + cancelRes2 = sceKernelWaitEventFlag(ef, 0x04, EVF_WAITMODE_OR, &cancelPat2, nullptr); + }); + WaitForEventFlagWaiters(ef, 2); + waitThreads = -1; + Require(sceKernelCancelEventFlag(ef, 0x05, &waitThreads) == SCE_OK && waitThreads == 2); + cancelT1.join(); + cancelT2.join(); + Require(cancelRes1 == SCE_KERNEL_ERROR_ECANCELED && cancelPat1 == 0); + Require(cancelRes2 == SCE_KERNEL_ERROR_ECANCELED && cancelPat2 == 0); + Require(sceKernelPollEventFlag(ef, 0x05, EVF_WAITMODE_AND, &pattern) == SCE_OK && pattern == 0x05); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "imm_sat", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); pattern = 0; Require(sceKernelWaitEventFlag(ef, 0x01, EVF_WAITMODE_AND | EVF_WAITMODE_CLEAR_PAT, &pattern, nullptr) == SCE_OK); From 1a74a6cdd9d3525115ccfe12dccb9185ad70fb5e Mon Sep 17 00:00:00 2001 From: thuanquang Date: Wed, 21 Oct 2026 10:57:29 +0700 Subject: [PATCH 128/318] fix(shader): keep a v_writelane lane known across computed-index window stores fdfawef324 notes that current LLVM spills SGPRs into VGPRs it allocates like any other register, so one window register can be an ordinary register early in a program and hold spill lanes later. ReadLaneEliminator only followed a spilled lane across M0-indexed window stores for slots never used as ordinary registers, so RESIDENT EVIL 4 (PPSA07411) and LEGO Horizon Adventures (PPSA14632) kept their reloaded descriptors as run-time values and stopped. A known lane can only come from a v_writelane chain, so keep it across a computed-index store instead; a constant-index store of the slot still ends the spill. This is fdfawef324's suggestion on #6277. That such a store misses the spill cannot be proven for these programs, so it is checked at run time where a replaced read depends on it: a backward liveness over each slot and lane whose known value a replaced read used puts a WindowLaneGuard before every computed-index store at which that lane is live, and the guard records a WindowLaneSpill fault when the store changes that lane of the slot. The driver reads the fault buffer back for every program with such a guard and reports the dispatch or draw as failed, so a broken assumption is no longer silent. This also covers the pure spill slots main already folds. Co-authored-by: fdfawef324 <287205748+fdfawef324@users.noreply.github.com> Co-Authored-By: Claude --- .../Graphics/src/BdaResources.cpp | 4 + .../Graphics/src/ShaderAddressResources.cpp | 2 +- .../libSceAgcDriver/tests/VectorWindow.cpp | 36 +++++ .../tests/execution/WindowLaneSpill.cpp | 133 ++++++++++++++-- core/shader/recompiler/BdaAbi.hpp | 2 +- .../IntermediateRepresentation/IrOpcode.hpp | 1 + .../src/IrOpcode.cpp | 2 + .../Optimization/ReadLaneEliminator.hpp | 1 + .../Optimization/src/ReadLaneEliminator.cpp | 145 +++++++++++++----- core/shader/recompiler/Recompiler.cpp | 2 + core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 2 + core/shader/recompiler/ShaderDiskCache.hpp | 2 +- .../SpirvBackend/SpirvEmitterInstructions.hpp | 1 + .../SpirvBackend/src/SpirvFlowEmitter.cpp | 10 ++ docs/dev/debt/shader.md | 1 + 16 files changed, 291 insertions(+), 54 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 1da043f87f..66fbaeeb29 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -282,6 +282,10 @@ void BdaResources::CheckFault() const { message << "shader stage " << report.stage << " called address 0x" << std::hex << report.address << " at program counter 0x" << report.instruction << ", which no linked callee of that call matches"; throw std::runtime_error(message.str()); } + if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::WindowLaneSpill) { + message << "shader stage " << report.stage << ": a relative write stored " << report.bytes << " in lane " << (report.address >> 32u) << " of vector window slot " << (report.address & 0xffffffffu) << ", which holds a scalar spilled with v_writelane_b32 that a later v_readlane_b32 was replaced with"; + throw std::runtime_error(message.str()); + } message << "BDA access failed: address=0x" << std::hex << report.address << " instruction=0x" << report.instruction << std::dec << " bytes=" << report.bytes << " stage=" << report.stage << " reason=" << static_cast(report.reason); if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Permission && table != nullptr) { const auto bytes = table->Bytes(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp index 2915b76a5a..f542724dc8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderAddressResources.cpp @@ -134,7 +134,7 @@ void ShaderResources::prepareAddressBindings(std::span sha if (tables != 0 && storesThroughAddressesCached(shader.program->spirv)) bdaReadOnly = false; usesFaultBuffer = usesFaultBuffer || faults != 0; const bool tableFaultsOnly = std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::ImageTableMap; }); - faultChecks = faultChecks || tables != 0 || (faults != 0 && (!tableFaultsOnly || shader.program->imageTableFaults != 0 || shader.program->poisonedSrtReads != 0 || loopGuarded)); + faultChecks = faultChecks || tables != 0 || (faults != 0 && (!tableFaultsOnly || shader.program->imageTableFaults != 0 || shader.program->poisonedSrtReads != 0 || shader.program->windowLaneGuards != 0 || loopGuarded)); } if (usesBda) { Require(context.bufferDeviceAddress, "buffer device address is not enabled"); diff --git a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp index a69e325898..6e07c138b6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/VectorWindow.cpp @@ -40,6 +40,13 @@ constexpr std::uint32_t SampleV8V2S0S8 = 0x00400802u; constexpr std::uint32_t AddV8V1V8 = 0x4a101101u; constexpr std::uint32_t BufferStoreDword = 0xe0700000u; constexpr std::uint32_t StoreV8WithS12 = 0x80030800u; +constexpr std::uint32_t AndM0GroupXOne = 0x877c8104u; +constexpr std::uint32_t MovreldV2V0 = 0x7e048500u; +constexpr std::uint32_t MoveS16Two = 0xbe900382u; +constexpr std::uint32_t BranchForwardTwo = 0xbf820002u; +constexpr std::uint32_t SubtractS16One = 0x80908110u; +constexpr std::uint32_t CompareS16NotZero = 0xbf078010u; +constexpr std::uint32_t BranchBackFourOnScc = 0xbf85fffcu; struct Shape { std::size_t functionArrays = 0; @@ -306,6 +313,35 @@ int main(int argc, char** argv) { return shape.functionArrays == 0 && shape.accessChains == 0; }); + passed &= run("a sampler dword spilled into a window register with ordinary writes keeps its definition past a relative write", [] { + const std::array userData{}; + const std::array spilled{AndM0GroupXOne, MoveV2Zero, MoveV3Zero, MoveV4One, WritelaneV4, FromS8Lane0, MovreldV2V0, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, BufferStoreDword, StoreV8WithS12, EndProgram}; + return Scan(RecompileProgram(spilled, userData)).functionArrays == 1; + }); + + passed &= run("a sampler dword spilled into a window register and overwritten by an ordinary write before its v_readlane stays a run-time value", [] { + const std::array userData{}; + const std::array overwritten{AndM0GroupXOne, MoveV2Zero, MoveV3Zero, WritelaneV4, FromS8Lane0, MovreldV2V0, MoveV4One, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, BufferStoreDword, StoreV8WithS12, EndProgram}; + try { + RecompileProgram(overwritten, userData); + } catch (const std::exception& error) { + return std::string_view(error.what()).find("GetSamplerResource dword 0 is not a valid runtime value; chain: ReadLane(LoadVectorWindow(Immediate), Immediate)") != std::string_view::npos; + } + return false; + }); + + passed &= run("a sampler dword spilled into a window register before a loop entered at its condition keeps its definition past the loop's relative write", [] { + const std::array userData{}; + const std::array code{AndM0GroupXOne, MoveV2Zero, MoveV3Zero, WritelaneV4, FromS8Lane0, MoveS16Two, BranchForwardTwo, MovreldV2V0, SubtractS16One, CompareS16NotZero, BranchBackFourOnScc, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, BufferStoreDword, StoreV8WithS12, EndProgram}; + return Scan(RecompileProgram(code, userData)).functionArrays == 1; + }); + + passed &= run("a sampler dword spilled into a window register with ordinary writes before a loop entered at its condition keeps its definition past the loop's relative write", [] { + const std::array userData{}; + const std::array code{AndM0GroupXOne, MoveV2Zero, MoveV3Zero, MoveV4One, WritelaneV4, FromS8Lane0, MoveS16Two, BranchForwardTwo, MovreldV2V0, SubtractS16One, CompareS16NotZero, BranchBackFourOnScc, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, BufferStoreDword, StoreV8WithS12, EndProgram}; + return Scan(RecompileProgram(code, userData)).functionArrays == 1; + }); + passed &= run("a sampler dword spilled into a relatively read register keeps its definition through v_readlane", [] { const std::array code{MoveM0GroupX, MoveV2Zero, MoveV3Zero, WritelaneV4, FromS8Lane0, MovrelsV1V4, ReadlaneS8, FromV4Lane0, ImageSampleLz2d, SampleV8V2S0S8, AddV8V1V8, BufferStoreDword, StoreV8WithS12, EndProgram}; const std::array userData{}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp index f597fc79ee..a18422c711 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/WindowLaneSpill.cpp @@ -7,7 +7,13 @@ #include #include #include +#include #include +#ifdef _WIN32 +#include +#else +#include +#endif namespace { @@ -15,7 +21,8 @@ using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; constexpr std::uint32_t Threads = 32; -alignas(256) std::array Output{}; +constexpr std::uint32_t WideThreads = 64; +alignas(256) std::array Output{}; alignas(256) constexpr std::array Code{ 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x00010004, 0xd7610003, 0x00010205, 0xd7610003, 0x00010406, @@ -29,28 +36,111 @@ alignas(256) constexpr std::array IndexedStoreCode{ 0xd7600009, 0x00010303, 0xd760000a, 0x00010503, 0xd760000b, 0x00010703, 0xe0702000, 0x80020001, 0xbf810000, }; +alignas(256) constexpr std::array ReusedRegisterCode{ + 0x7e060300, 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x00010004, 0xd7610003, 0x00010205, 0xd7610003, + 0x00010406, 0xd7610003, 0x00010607, 0x7e048500, 0xbe840380, 0xbe850380, 0xbe860380, 0xbe870380, 0xd7600008, + 0x00010103, 0xd7600009, 0x00010303, 0xd760000a, 0x00010503, 0xd760000b, 0x00010703, 0xe0702000, 0x80020001, + 0xbf810000, +}; + +alignas(256) constexpr std::array OverwrittenSpillCode{ + 0x877c8104, 0x7e040280, 0x7e060280, 0xd7610004, 0x00010008, 0x7e048500, 0x7e0802f2, 0xd7600008, 0x00010104, + 0xf09c0f08, 0x00400802, 0xe0700000, 0x80030800, 0xbf810000, +}; + +alignas(256) constexpr std::array AliasingStoreCode{ + 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x0001000d, 0x7e040287, 0x7e048500, + 0xd7600008, 0x00010103, 0x7e040208, 0xe0702000, 0x80010201, 0xbf810000, +}; + +alignas(256) constexpr std::array WideAliasingStoreCode{ + 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x0001500d, 0x7e040287, 0x7e048500, + 0xd7600008, 0x00015103, 0x7e040208, 0xe0702000, 0x80010201, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreAfterReadCode{ + 0x7e020300, 0xbefc030c, 0x7e048500, 0xd7610003, 0x0001000d, 0xd7600008, + 0x00010103, 0x7e048500, 0x7e040208, 0xe0702000, 0x80010201, 0xbf810000, +}; + +constexpr std::uint32_t Spilled = 0x1234u; + +class StderrCapture { +public: + StderrCapture() { + std::fflush(stderr); + file = std::tmpfile(); + Require(file != nullptr, "cannot open a temporary file for stderr"); +#ifdef _WIN32 + saved = _dup(_fileno(stderr)); + _dup2(_fileno(file), _fileno(stderr)); +#else + saved = dup(fileno(stderr)); + dup2(fileno(file), fileno(stderr)); +#endif + } + std::string Finish() { + std::fflush(stderr); +#ifdef _WIN32 + _dup2(saved, _fileno(stderr)); + _close(saved); +#else + dup2(saved, fileno(stderr)); + close(saved); +#endif + std::rewind(file); + std::string text; + char buffer[4096]; + for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read); + std::fclose(file); + std::fwrite(text.data(), 1, text.size(), stderr); + return text; + } + +private: + std::FILE* file = nullptr; + int saved = -1; +}; + std::array BufferDescriptor(const void* data, std::uint32_t count) { const auto address = reinterpret_cast(data); return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x11016facu}; } -void Run(AgcDriver::VulkanDevice& device, std::span code) { - Output.fill(0xdeadbeefu); - std::vector userData(16, 0u); - const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); - std::copy(output.begin(), output.end(), userData.begin() + 4); +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span userData, std::uint32_t waveSize = Threads) { const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; - const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{waveSize, 1, 1}, 0, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, - {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, device.Target(), {0, 0, 0, 128} }; request.useCache = false; - const auto result = ShaderRecompiler::Recompile(request); + return ShaderRecompiler::Recompile(request); +} + +std::string Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t relativeIndex = 0, std::uint32_t waveSize = Threads) { + Output.fill(0xdeadbeefu); + std::vector userData(16, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(output.begin(), output.end(), userData.begin() + 4); + userData[12] = relativeIndex; + userData[13] = Spilled; + const auto result = Compile(device, code, userData, waveSize); + StderrCapture log; device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); device.WaitIdle(); + return log.Finish(); +} + +constexpr std::string_view Overwritten = "which holds a scalar spilled with v_writelane_b32 that a later v_readlane_b32 was replaced with"; + +void CheckSpilled(const std::string& log, const std::string& name, std::uint32_t lanes = Threads) { + Require(log.find(Overwritten) == std::string::npos, name + ": reported an overwritten spill: " + log); + for (std::uint32_t tid = 0; tid < lanes; ++tid) { + Require(Output[tid] == Spilled, name + ": lane " + std::to_string(tid) + " stored " + std::to_string(Output[tid]) + ", expected the spilled " + std::to_string(Spilled)); + } } void Check(const std::string& name) { @@ -59,6 +149,17 @@ void Check(const std::string& name) { } } +void CheckRefused(AgcDriver::VulkanDevice& device, std::span code, const std::string& name) { + const std::vector userData(16, 0u); + std::string error; + try { + static_cast(Compile(device, code, userData)); + } catch (const std::exception& thrown) { + error = thrown.what(); + } + Require(error.find("GetSamplerResource dword 0 is not a valid runtime value") != std::string::npos, name + ": " + (error.empty() ? std::string("recompiled") : error)); +} + } int main() { @@ -73,6 +174,20 @@ int main() { Check("window lane spill"); Run(*device, IndexedStoreCode); Check("window lane spill past an indexed store"); + CheckRefused(*device, OverwrittenSpillCode, "window lane spill overwritten by an ordinary write before its v_readlane"); + Run(*device, ReusedRegisterCode); + Check("window lane spill into a register with ordinary writes past an indexed store"); + CheckSpilled(Run(*device, AliasingStoreCode, 0), "window lane spill past an indexed store with m0 = 0"); + const auto aliased = Run(*device, AliasingStoreCode, 1); + Require(aliased.find(Overwritten) != std::string::npos, "window lane spill overwritten by an indexed store with m0 = 1 was not reported: " + aliased); + CheckSpilled(Run(*device, StoreAfterReadCode, 1), "window lane spill overwritten by an indexed store with m0 = 1 after its v_readlane"); + if (device->Target().subgroupSize >= WideThreads) { + CheckSpilled(Run(*device, WideAliasingStoreCode, 0, WideThreads), "wave64 window lane spill in lane 40 past an indexed store with m0 = 0", WideThreads); + const auto wide = Run(*device, WideAliasingStoreCode, 1, WideThreads); + Require(wide.find("stored 40 in lane 40 of vector window slot 1") != std::string::npos, "wave64 window lane spill in lane 40 overwritten by an indexed store with m0 = 1 was not reported: " + wide); + } else { + std::printf("wave64 cases skipped, subgroup size %u\n", device->Target().subgroupSize); + } std::puts("window lane spill tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index 0c84230b80..f5df5fcbcd 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -35,7 +35,7 @@ inline bool RangesOrdered(const Range* ranges, std::size_t count) { } enum class FaultState : std::uint32_t { Empty, Writing, Ready }; -enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned, InvalidDescriptor, ImageTable, UnlinkedCall }; +enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned, InvalidDescriptor, ImageTable, UnlinkedCall, WindowLaneSpill }; struct Fault { FaultState state; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index c1ed5e72ed..c6b34841ba 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -74,6 +74,7 @@ enum class IrOpcode : std::uint16_t { Reference, ReferenceU32, UnlinkedCall, + WindowLaneGuard, GetUserData, GetShaderBase, ShaderClock, diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index b3622f4c6a..22ec648a27 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -115,6 +115,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("Reference", Void, U1), makeMeta("ReferenceU32", Void, U32), makeMeta("UnlinkedCall", Void, U32, U32, U32), + makeMeta("WindowLaneGuard", Void, U32, U32, U32, U32, U32), makeMeta("GetUserData", U32, ScalarReg), makeMeta("GetShaderBase", U64), makeMeta("ShaderClock", U64), @@ -894,6 +895,7 @@ bool IrOpcodeHasSideEffects(IrOpcode opcode) { case IrOpcode::Reference: case IrOpcode::ReferenceU32: case IrOpcode::UnlinkedCall: + case IrOpcode::WindowLaneGuard: case IrOpcode::SetTessellationAttribute: case IrOpcode::InterpolateHostP2: case IrOpcode::SetThreadBitScalarRegister: diff --git a/core/shader/recompiler/Optimization/include/Optimization/ReadLaneEliminator.hpp b/core/shader/recompiler/Optimization/include/Optimization/ReadLaneEliminator.hpp index a52432123a..8fdbe75a07 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ReadLaneEliminator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ReadLaneEliminator.hpp @@ -8,6 +8,7 @@ namespace ShaderRecompiler { struct ReadLaneEliminationStats { std::uint32_t rewrittenReads = 0; + std::uint32_t guardedStores = 0; }; class ReadLaneEliminator { diff --git a/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp b/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp index 97fbe47cb2..d2ebf74e9b 100644 --- a/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp @@ -1,6 +1,8 @@ #include "Optimization/ReadLaneEliminator.hpp" #include #include +#include +#include #include #include #include @@ -52,44 +54,13 @@ struct LaneFlow { struct WindowFlows { const IrProgram* program = nullptr; - std::unordered_set spillSlots; std::unordered_map flows; std::unordered_set computing; + std::vector>* folded = nullptr; }; thread_local WindowFlows* windowFlows = nullptr; -std::unordered_set collectLaneSpillSlots(const IrProgram& program) { - std::unordered_set candidates; - std::unordered_set rejected; - const auto immediateSlot = [](const IrValue& inst, std::uint32_t& slot) { - const IrValue* index = inst.Argument(0)->Resolve(); - if (!isImmediateU32(*index)) return false; - slot = index->ImmediateU32(); - return true; - }; - for (const auto& block : program.Blocks()) { - for (const IrValue* inst : block->Instructions()) { - std::uint32_t slot = 0; - if (inst->Opcode() == IrOpcode::StoreVectorWindow) { - if (!immediateSlot(*inst, slot)) continue; - candidates.insert(slot); - if (inst->Argument(1)->Resolve()->Opcode() != IrOpcode::WriteLane) rejected.insert(slot); - } else if (inst->Opcode() == IrOpcode::LoadVectorWindow) { - if (!immediateSlot(*inst, slot)) continue; - candidates.insert(slot); - for (const IrUse& use : inst->OperandUses()) { - const bool laneRead = use.user->Opcode() == IrOpcode::ReadLane && use.operand == 0u; - const bool chained = use.user->Opcode() == IrOpcode::WriteLane && use.operand == 2u; - if (!laneRead && !chained) rejected.insert(slot); - } - } - } - } - for (const auto slot : rejected) candidates.erase(slot); - return candidates; -} - LaneState windowLoadLane(const IrValue* load, std::uint32_t lane, std::uint32_t waveSize); LaneState storedLane(IrValue* value, std::uint32_t slot, std::uint32_t lane, std::uint32_t waveSize, const LaneFlow& flow) { @@ -117,7 +88,6 @@ const LaneFlow* laneFlow(std::uint32_t slot, std::uint32_t lane, std::uint32_t w if (const auto found = flows.flows.find(key); found != flows.flows.end()) return &found->second; if (!flows.computing.insert(key).second) return nullptr; LaneFlow flow; - const bool spill = flows.spillSlots.contains(slot); const auto& blocks = flows.program->Blocks(); constexpr int MaxPasses = 64; bool settled = false; @@ -144,11 +114,7 @@ const LaneFlow* laneFlow(std::uint32_t slot, std::uint32_t lane, std::uint32_t w } } else if (inst->Opcode() == IrOpcode::StoreVectorWindow) { const IrValue* index = inst->Argument(0)->Resolve(); - if (!isImmediateU32(*index)) { - if (!spill) running = {LaneState::Kind::Unknown, nullptr}; - continue; - } - if (index->ImmediateU32() != slot) continue; + if (!isImmediateU32(*index) || index->ImmediateU32() != slot) continue; running = storedLane(inst->Argument(1), slot, lane, waveSize, flow); } } @@ -177,8 +143,98 @@ LaneState windowLoadLane(const IrValue* load, std::uint32_t lane, std::uint32_t WindowLane windowLane(IrValue* load, std::uint32_t lane, std::uint32_t waveSize) { const LaneState state = windowLoadLane(load, lane, waveSize); - if (state.kind == LaneState::Kind::Known) return {state.value, true}; - return {}; + if (state.kind != LaneState::Kind::Known) return {}; + if (windowFlows != nullptr && windowFlows->folded != nullptr) windowFlows->folded->emplace_back(load, lane); + return {state.value, true}; +} + +IrValue* laneSource(IrValue* value, std::uint32_t lane, std::uint32_t waveSize) { + for (;;) { + value = value->Resolve(); + if (value->Opcode() == IrOpcode::WriteLane) { + const IrValue* selector = value->Argument(1)->Resolve(); + if (!isImmediateU32(*selector) || selector->ImmediateU32() % waveSize == lane) return nullptr; + value = value->Argument(2); + continue; + } + if (value->Opcode() != IrOpcode::LoadVectorWindow || !isImmediateU32(*value->Argument(0)->Resolve())) return nullptr; + return value; + } +} + +IrValue& immediate(IrProgram& program, std::uint32_t value) { + IrValue& constant = program.CreateValue(IrOpcode::Void, IrType::U32); + constant.SetImmediateU32(value); + return constant; +} + +std::size_t guardSpilledLanes(IrProgram& program, const std::vector>& folded, std::uint32_t waveSize) { + const auto keyOf = [](std::uint32_t slot, std::uint32_t lane) { return (static_cast(slot) << 32u) | lane; }; + std::map> used; + std::vector work; + const auto use = [&](IrValue* load, std::uint32_t lane) { + const auto key = keyOf(load->Argument(0)->Resolve()->ImmediateU32(), lane); + if (!used[key].insert(load).second) return false; + work.push_back(key); + return true; + }; + for (const auto& [load, lane] : folded) use(load, lane); + std::unordered_map> guarded; + while (!work.empty()) { + const auto key = work.back(); + work.pop_back(); + const auto slot = static_cast(key >> 32u); + const auto lane = static_cast(key); + const auto& usedLoads = used[key]; + std::unordered_map liveIn; + for (bool changed = true; changed;) { + changed = false; + for (auto block = program.Blocks().rbegin(); block != program.Blocks().rend(); ++block) { + bool live = false; + for (const IrBlock* successor : (*block)->Successors()) live = live || liveIn[successor]; + for (auto inst = (*block)->Instructions().rbegin(); inst != (*block)->Instructions().rend(); ++inst) { + IrValue* value = *inst; + if (value->Opcode() == IrOpcode::LoadVectorWindow) { + live = live || usedLoads.contains(value); + continue; + } + if (value->Opcode() != IrOpcode::StoreVectorWindow) continue; + const IrValue* index = value->Argument(0)->Resolve(); + if (!isImmediateU32(*index)) { + if (live) guarded[value].insert(key); + continue; + } + if (index->ImmediateU32() != slot) continue; + if (live) { + if (IrValue* source = laneSource(value->Argument(1), lane, waveSize); source != nullptr && use(source, lane)) changed = true; + } + live = false; + } + if (const auto found = liveIn.find(block->get()); found == liveIn.end() || found->second != live) { + changed = changed || live || found != liveIn.end(); + liveIn[block->get()] = live; + } + } + } + } + std::size_t guards = 0; + for (const auto& block : program.Blocks()) { + for (IrValue* store : std::vector(block->Instructions().begin(), block->Instructions().end())) { + const auto found = guarded.find(store); + if (found == guarded.end()) continue; + IrValue& current = program.CreateValue(IrOpcode::LoadVectorWindow, IrType::U32); + current.AddArgument(store->Argument(0)); + block->InsertInstructionBefore(store, ¤t); + for (const auto key : found->second) { + IrValue& guard = program.CreateValue(IrOpcode::WindowLaneGuard, IrType::Void); + for (IrValue* argument : {store->Argument(0), store->Argument(1), ¤t, &immediate(program, static_cast(key >> 32u)), &immediate(program, static_cast(key))}) guard.AddArgument(argument); + block->InsertInstructionBefore(store, &guard); + ++guards; + } + } + } + if (guards != 0u) program.Info().usesFaultBuffer = true; + return guards; } ChainResult searchChain(IrValue* value, std::uint32_t lane, std::uint32_t waveSize) { @@ -362,7 +418,7 @@ ReadLaneEliminationStats ReadLaneEliminator::Eliminate(IrProgram& program, std:: } WindowFlows flows; flows.program = &program; - flows.spillSlots = collectLaneSpillSlots(program); + std::vector> folded; struct FlowScope { ~FlowScope() { windowFlows = nullptr; } } flowScope; @@ -381,6 +437,7 @@ ReadLaneEliminationStats ReadLaneEliminator::Eliminate(IrProgram& program, std:: const std::uint32_t lane = selector->ImmediateU32() % waveSize; const ChainResult chain = searchChain(inst->Argument(0), lane, waveSize); if (chain.write != nullptr) { + if (chain.value->Opcode() == IrOpcode::LoadVectorWindow) folded.emplace_back(chain.value, lane); inst->ReplaceUsesWith(chain.scalar, true); stats.rewrittenReads++; continue; @@ -397,10 +454,14 @@ ReadLaneEliminationStats ReadLaneEliminator::Eliminate(IrProgram& program, std:: } PhiMap phiMap; - inst->ReplaceUsesWith(getRealValue(program, phiMap, chain.value, lane, waveSize), true); + flows.folded = &folded; + IrValue* real = getRealValue(program, phiMap, chain.value, lane, waveSize); + flows.folded = nullptr; + inst->ReplaceUsesWith(real, true); stats.rewrittenReads++; } } + stats.guardedStores = static_cast(guardSpilledLanes(program, folded, waveSize)); return stats; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ad94823d64..763f88f3ed 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -666,6 +666,8 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) } result.bdaAbiVersion = program.Info().usesDma || program.Info().usesFaultBuffer ? request.target.bdaAbiVersion : 0u; + result.windowLaneGuards = 0; + for (const auto& block : program.Blocks()) result.windowLaneGuards += static_cast(std::ranges::count_if(block->Instructions(), [](const IrValue* value) { return value->Opcode() == IrOpcode::WindowLaneGuard; })); result.memoryOffsetDword = bindings.layout.memoryOffsetDword; result.shaderDataDwords = bindings.layout.ShaderDataDwords(); result.imageMetadataDword = bindings.layout.ImageMetadataDword(); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 127ed88d5b..db07fdc684 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -653,6 +653,7 @@ struct CompiledShaderArtifact { std::vector runtimeImageResources; bool registrationModule = false; std::uint32_t bdaAbiVersion = 0; + std::uint32_t windowLaneGuards = 0; std::uint32_t runtimeAbiVersion = RuntimeAbi::Version; std::vector vertexInputs; std::int32_t vertexOffsetSgpr = -1; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index bcda9f0c40..406136a9b6 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -469,6 +469,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.runtimeAbiVersion); writer.Values(std::span(result.spirv.Words())); writer.Value(result.bdaAbiVersion); + writer.Value(result.windowLaneGuards); writer.Value(result.memoryOffsetDword); writer.Value(result.shaderDataDwords); writer.Value(result.imageMetadataDword); @@ -527,6 +528,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.runtimeAbiVersion); result.spirv = reader.Spirv(); reader.Value(result.bdaAbiVersion); + reader.Value(result.windowLaneGuards); reader.Value(result.memoryOffsetDword); reader.Value(result.shaderDataDwords); reader.Value(result.imageMetadataDword); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index e5acfa71f9..abd799b205 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 44; +inline constexpr std::uint32_t FormatVersion = 45; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index a8f03e192a..c442047480 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -221,6 +221,7 @@ std::uint32_t EmitFPCmpClass32(SpirvEmitterState& state, std::uint32_t value, st void EmitReference(SpirvValueEmitContext& context); void EmitReferenceU32(SpirvValueEmitContext& context); void EmitUnlinkedCall(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t low, std::uint32_t high, std::uint32_t programCounter); +void EmitWindowLaneGuard(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t index, std::uint32_t value, std::uint32_t current, std::uint32_t slot, std::uint32_t lane); void EmitControlNop(SpirvValueEmitContext& context); void EmitWaitcnt(SpirvValueEmitContext& context); void EmitSendmsg(SpirvValueEmitContext& context); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 4cd661b16a..9c34192ed8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -308,6 +308,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::Reference: return Invoke(EmitReference, ctx, inst); case IrOpcode::ReferenceU32: return Invoke(EmitReferenceU32, ctx, inst); case IrOpcode::UnlinkedCall: return Invoke(EmitUnlinkedCall, ctx, inst); + case IrOpcode::WindowLaneGuard: return Invoke(EmitWindowLaneGuard, ctx, inst); case IrOpcode::GetUserData: return Invoke(EmitGetUserData, ctx, inst); case IrOpcode::GetShaderBase: return Invoke(EmitGetShaderBase, ctx, inst); case IrOpcode::ShaderClock: return Invoke(EmitShaderClock, ctx, inst); @@ -920,6 +921,15 @@ void EmitUnlinkedCall(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint StopBdaInvocationIf(state, ConstantBool(state, true)); } +void EmitWindowLaneGuard(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t index, std::uint32_t value, std::uint32_t current, std::uint32_t slot, std::uint32_t lane) { + auto& state = ctx.state; + if (state.faultBufferVariable == 0) ctx.Fail(inst, "checks a spilled lane without a fault buffer"); + const auto boolean = TypeBool(state); + const auto ownLane = Binary(state, spv::OpIEqual, boolean, EmitSubgroupLocalInvocationId(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), lane, ConstantU32(state, state.program.WaveSize() - 1u))); + const auto overwritten = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpIEqual, boolean, index, slot), Binary(state, spv::OpINotEqual, boolean, value, current)); + EmitIfCondition(state, Binary(state, spv::OpLogicalAnd, boolean, ownLane, overwritten), [&] { RecordBdaFaultWords(state, slot, lane, value, ConstantU32(state, 0u), BdaAbi::FaultReason::WindowLaneSpill); }); +} + void EmitControlNop(SpirvValueEmitContext& context) { EmitVoid(context); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d76932d773..aa27f24aae 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -166,6 +166,7 @@ - [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. Outside every region of its calls the pair is free to use wherever no return address can reach: a dataflow over the decoded instructions follows each dword of the pair from the calls that write it until a scalar load, SALU or VALU result is known to overwrite it, and an instruction there that may read a dword still holding a return address throws; SOPC compares are exempt, as they only set SCC. A VALU mask covers one SGPR in wave32 and a pair in wave64. When the graph is built without the wave size, the dataflow runs for both: a program that escapes under both throws there, and one that escapes under only one throws when it is translated at that size. - [Static scalar call link pairs](../../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) each serve one call, or several calls whose regions are disjoint, each call closed by the first return reading the pair after its target. 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 zero-initialised function-scope storage 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. [`IndexedWindowSlots`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp) bounds each relative index through the IR (bit-field extracts, masks, shifts, adds, selects, phis, `ReadFirstLane` and the window stores that reach a load); the storage is an array of the slots below the largest bound that is under half the window, and one function variable per slot after it. A constant index uses the array element or the slot's variable; a relative access whose index has no such bound, or lands past the array at run time, indexes the array below its size and otherwise branches through an `OpSwitch` over the slot variables, so the bound only decides speed. When no index has a bound under half the window, the whole window is the array, as with `APS5_NO_WINDOW_NARROWING=1` (a recompiler switch in the shader cache key). Measured on an NVIDIA GeForce RTX 3060 (driver 615.78.08, Linux) with a compute program whose 167-register window an ALU loop reads and writes in full and whose relative accesses take `m0 = s8 & 3`: a 2048-group dispatch took 70.2 ms in wave32 and 123.3 ms in wave64 with the whole window in the array (as before narrowing) and 1.9-2.1 ms and 7.8 ms narrowed; with a second relative access whose index has no bound and lands past the array at run time (index 50 or 166), 113 ms and 5.8-6.0 ms in wave32. In the PPSA08427 (Killing Floor 3) hub it made no measurable difference to presents or to the GPU time of the eight most expensive programs. Whether the host compiler keeps the array and the slot variables in registers or spills them to scratch 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. A whole-word use that the masked-select pass keeps gets the composed word described under the packed pixel ancillary word. 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. +- [Lane spills read back with `v_readlane_b32`](../../../core/shader/recompiler/Optimization/src/ReadLaneEliminator.cpp) (shader recompiler) - not verified for the guest compiler: a relative write (`v_movreld_b32`, `v_movrelsd_b32`, `v_movrelsd_2_b32`, `v_swaprel_b32`) with a computed index is taken to miss a register between a `v_writelane_b32` of a lane and a later `v_readlane_b32` of it, so the read becomes the spilled scalar. That holds when the compiler keeps spill registers out of its relatively indexed arrays. Each such write that a replaced read depends on is checked at run time: one that changes that lane of that register records a fault, and the dispatch or draw fails with `which holds a scalar spilled with v_writelane_b32`, where the console would read the new value ([m0 = 1 counterexample](https://github.com/boykopovar/AnyPS5/pull/6277#issuecomment-6215244790)). An inactive or out-of-range lane writes nothing. A constant-index write of the register still ends the spill. - [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. - [BDA anchor halfword reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) retain volatile byte loads even for noncoherent memory. On RX 6650 XT / RADV Mesa 26.2.4, the valid SPIR-V for two adjacent nonvolatile byte loads returned `0x80a0` for bytes `0xa0, 0xa1`, where lookup-based reads returned `0xa1a0`. Adding the volatile operand makes the same GPU exactness fixture pass, including mixed direct/fallback lanes. The driver transformation responsible has not been isolated; removing this restriction needs equivalent readback coverage on the affected driver. Single-byte reads keep their original access mask. - [BDA span reads](../../../core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp) (shader recompiler) - `read_bda_span` is one selection: one arm calls `load_bda_span`, a `DontInline` function that holds the in-place dword loads and calls nothing, and the other arm is the per-dword byte fallback. Two other shapes of the same reads each hang a group of NVIDIA drivers, where a dispatch never finishes and the device is lost. With the loads emitted in the arm itself, a dispatch whose lanes took both arms hung on an RTX 3060 and an RTX 4060 Laptop GPU (Windows, driver 591.86) and an RTX 5060 (Linux, driver 590.48.01) ([#3291](https://github.com/boykopovar/AnyPS5/issues/3291)). With the loads and the fallback as two selections, the first under the span's condition and the second under its negation, it hung on an RTX 4070 Laptop GPU (Windows, drivers 595.97 and 596.36), an RTX 5080 (Windows, driver 610.62) and an RTX 4090 (Linux, driver 610.57.04) ([#4975](https://github.com/boykopovar/AnyPS5/issues/4975)). The current shape is measured on the RTX 4090 with driver 610.57.04 only, where it does not hang with or without SPIRV-Tools; a shape with the loads in their own `DontInline` function was reported passing on driver 591.86 in #3291. The cause inside the driver is not known. From c416e3aca3cfc8f18f27aad07e316ac47640a746 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 10:17:32 -0300 Subject: [PATCH 129/318] feat(libkernel): implement shared event flags and sceKernelCloseEventFlag Measured on firmware 13.60: attribute 0x100 makes a flag openable by name, and only such flags can be opened. Each open returns a new handle on the same flag, which sceKernelCloseEventFlag releases. Closing a creator handle and deleting through an opened one return EPERM. Two live shared flags cannot share a name, names of 32 or more bytes return ENAMETOOLONG, and a missing name returns ESRCH. After the creator deletes the flag, an opened handle polls and waits with EACCES and sets, clears and cancels with no effect. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/EventFlag/src/EventFlag.cpp | 232 +++++++++++------- .../prx/libkernel_sys/Export.Discovery.cpp | 5 - core/libs/tests/GuestKernelErrors.cpp | 83 ++++++- docs/dev/debt/libkernel.md | 3 +- 4 files changed, 221 insertions(+), 102 deletions(-) diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index e9d51f67c1..4d2e1f7d1d 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -14,24 +14,26 @@ #include #include #include +#include #include static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; -static constexpr int SCE_KERNEL_ERROR_ENOENT = 0x80020002; static constexpr int SCE_KERNEL_ERROR_ESRCH = 0x80020003; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EACCES = 0x8002000D; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; +static constexpr int SCE_KERNEL_ERROR_ENAMETOOLONG = 0x8002003F; static constexpr int SCE_KERNEL_ERROR_ECANCELED = 0x80020055; static constexpr uint32_t EVF_ATTR_TH_FIFO = 0x01; static constexpr uint32_t EVF_ATTR_TH_PRIO = 0x02; static constexpr uint32_t EVF_ATTR_SINGLE = 0x10; static constexpr uint32_t EVF_ATTR_MULTI = 0x20; -static constexpr uint32_t EVF_ATTR_UNKNOWN_0x100 = 0x100; +static constexpr uint32_t EVF_ATTR_SHARED = 0x100; +static constexpr size_t EVF_NAME_CAPACITY = 32; static constexpr uint32_t EVF_WAITMODE_AND = 0x01; static constexpr uint32_t EVF_WAITMODE_OR = 0x02; static constexpr uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; @@ -52,6 +54,7 @@ struct KernelEventFlagPrivate { std::string _name; uint64_t _pattern = 0; bool _multipleWaiters = false; + bool _shared = false; bool _deleted = false; int _waiters = 0; uint64_t _cancelGeneration = 0; @@ -87,20 +90,33 @@ struct KernelEventFlagPrivate { namespace { -std::shared_mutex eventFlagRegistryMutex; -std::unordered_map> eventFlagRegistry; +struct EventFlagHandle { + std::shared_ptr flag; + bool opened = false; +}; -std::shared_ptr ResolveEventFlag(KernelEventFlag handle) { +std::shared_mutex eventFlagRegistryMutex; +std::unordered_map> eventFlagRegistry; + +std::shared_ptr ResolveEventFlag(KernelEventFlag handle) { if (!handle) return {}; std::shared_lock lock(eventFlagRegistryMutex); const auto it = eventFlagRegistry.find(handle); - return it == eventFlagRegistry.end() ? std::shared_ptr{} : it->second; + return it == eventFlagRegistry.end() ? std::shared_ptr{} : it->second; } -void RetireEventFlag(KernelEventFlag handle, const std::shared_ptr& owner) { +KernelEventFlag RegisterEventFlag(const std::shared_ptr& entry) { + const auto handle = reinterpret_cast(entry.get()); + eventFlagRegistry.emplace(handle, entry); + return handle; +} + +bool RetireEventFlag(KernelEventFlag handle, const std::shared_ptr& owner) { std::unique_lock lock(eventFlagRegistryMutex); const auto it = eventFlagRegistry.find(handle); - if (it != eventFlagRegistry.end() && it->second == owner) eventFlagRegistry.erase(it); + if (it == eventFlagRegistry.end() || it->second != owner) return false; + eventFlagRegistry.erase(it); + return true; } } @@ -133,137 +149,175 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; if ((attr & EVF_ATTR_SINGLE) && (attr & EVF_ATTR_MULTI)) return SCE_KERNEL_ERROR_EINVAL; if ((attr & EVF_ATTR_TH_FIFO) && (attr & EVF_ATTR_TH_PRIO)) return SCE_KERNEL_ERROR_EINVAL; - if ((attr & ~(EVF_ATTR_TH_FIFO | EVF_ATTR_TH_PRIO | EVF_ATTR_SINGLE | EVF_ATTR_MULTI | EVF_ATTR_UNKNOWN_0x100)) != 0) return SCE_KERNEL_ERROR_EINVAL; - if (attr & EVF_ATTR_UNKNOWN_0x100) NotImplemented_nid_no_patch("sceKernelCreateEventFlag attribute 0x100"); + if ((attr & ~(EVF_ATTR_TH_FIFO | EVF_ATTR_TH_PRIO | EVF_ATTR_SINGLE | EVF_ATTR_MULTI | EVF_ATTR_SHARED)) != 0) return SCE_KERNEL_ERROR_EINVAL; + if (std::strlen(name) >= EVF_NAME_CAPACITY) return SCE_KERNEL_ERROR_ENAMETOOLONG; + KernelEventFlag handle = nullptr; std::shared_ptr flag; try { flag = std::make_shared(); flag->_name = name; flag->_pattern = init_pattern; flag->_multipleWaiters = (attr & EVF_ATTR_MULTI) != 0; + flag->_shared = (attr & EVF_ATTR_SHARED) != 0; + auto entry = std::make_shared(EventFlagHandle{flag, false}); std::unique_lock lock(eventFlagRegistryMutex); - eventFlagRegistry.emplace(flag.get(), flag); + if (flag->_shared) { + for (const auto& [other, otherEntry] : eventFlagRegistry) { + if (otherEntry->opened || !otherEntry->flag->_shared) continue; + std::lock_guard flagLock(otherEntry->flag->_mutex); + if (!otherEntry->flag->_deleted && otherEntry->flag->_name == flag->_name) return SCE_KERNEL_ERROR_EPERM; + } + } + handle = RegisterEventFlag(entry); } catch (const std::bad_alloc&) { return SCE_KERNEL_ERROR_ENOMEM; } - *ef = flag.get(); - if (TraceSync()) std::fprintf(stderr, "[evf] create %p '%s' attr=0x%x init=0x%llx\n", static_cast(flag.get()), name, attr, static_cast(init_pattern)); + *ef = handle; + if (TraceSync()) std::fprintf(stderr, "[evf] create %p '%s' attr=0x%x init=0x%llx\n", static_cast(handle), name, attr, static_cast(init_pattern)); return SCE_OK; } int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name) { if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; - std::shared_lock lock(eventFlagRegistryMutex); - for (const auto& [handle, flag] : eventFlagRegistry) { - std::lock_guard flagLock(flag->_mutex); - if (flag->_deleted || flag->_name != name) continue; - *ef = handle; - return SCE_OK; + try { + std::unique_lock lock(eventFlagRegistryMutex); + std::shared_ptr target; + for (const auto& [handle, entry] : eventFlagRegistry) { + if (entry->opened || !entry->flag->_shared) continue; + std::lock_guard flagLock(entry->flag->_mutex); + if (entry->flag->_deleted || entry->flag->_name != name) continue; + target = entry->flag; + break; + } + if (!target) return SCE_KERNEL_ERROR_ESRCH; + *ef = RegisterEventFlag(std::make_shared(EventFlagHandle{std::move(target), true})); + } catch (const std::bad_alloc&) { + return SCE_KERNEL_ERROR_ENOMEM; } - return SCE_KERNEL_ERROR_ENOENT; + return SCE_OK; +} + +int APS5_VABI sceKernelCloseEventFlag(KernelEventFlag ef) { + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; + if (!entry->opened) return SCE_KERNEL_ERROR_EPERM; + return RetireEventFlag(ef, entry) ? SCE_OK : SCE_KERNEL_ERROR_ESRCH; } int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; - std::unique_lock lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - ef->_deleted = true; - ef->_changed.NotifyAll(); - ef->_changed.Wait(lock, [&] { return ef->_waiters == 0; }); + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; + if (entry->opened) return SCE_KERNEL_ERROR_EPERM; + KernelEventFlagPrivate* flag = entry->flag.get(); + std::unique_lock lock(flag->_mutex); + if (flag->_deleted) return SCE_KERNEL_ERROR_ESRCH; + flag->_deleted = true; + flag->_changed.NotifyAll(); + flag->_changed.Wait(lock, [&] { return flag->_waiters == 0; }); lock.unlock(); - RetireEventFlag(ef, keepAlive); + RetireEventFlag(ef, entry); return SCE_OK; } int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, uint64_t bit_pattern) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; - std::lock_guard lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - ef->_pattern |= bit_pattern; - if (TraceSync()) std::fprintf(stderr, "[evf] set %p '%s' |=0x%llx -> 0x%llx\n", static_cast(ef), ef->_name.c_str(), static_cast(bit_pattern), static_cast(ef->_pattern)); - for (auto* waiter = ef->_waiterHead; waiter != nullptr; waiter = waiter->next) { - if (!waiter->satisfied && IsSatisfied(ef->_pattern, waiter->bitPattern, waiter->waitMode)) { + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; + KernelEventFlagPrivate* flag = entry->flag.get(); + std::lock_guard lock(flag->_mutex); + if (flag->_deleted) return entry->opened ? SCE_OK : SCE_KERNEL_ERROR_ESRCH; + flag->_pattern |= bit_pattern; + if (TraceSync()) std::fprintf(stderr, "[evf] set %p '%s' |=0x%llx -> 0x%llx\n", static_cast(ef), flag->_name.c_str(), static_cast(bit_pattern), static_cast(flag->_pattern)); + for (auto* waiter = flag->_waiterHead; waiter != nullptr; waiter = waiter->next) { + if (!waiter->satisfied && IsSatisfied(flag->_pattern, waiter->bitPattern, waiter->waitMode)) { waiter->satisfied = true; - waiter->resultPattern = ef->_pattern; + waiter->resultPattern = flag->_pattern; if (waiter->waitMode & EVF_WAITMODE_CLEAR_ALL) { - ef->_pattern = 0; + flag->_pattern = 0; } else if (waiter->waitMode & EVF_WAITMODE_CLEAR_PAT) { - ef->_pattern &= ~waiter->bitPattern; + flag->_pattern &= ~waiter->bitPattern; } } } - ef->_changed.NotifyAll(); + flag->_changed.NotifyAll(); return SCE_OK; } int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, uint64_t bit_pattern) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; - std::lock_guard lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - ef->_pattern &= bit_pattern; + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; + KernelEventFlagPrivate* flag = entry->flag.get(); + std::lock_guard lock(flag->_mutex); + if (flag->_deleted) return entry->opened ? SCE_OK : SCE_KERNEL_ERROR_ESRCH; + flag->_pattern &= bit_pattern; return SCE_OK; } int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, uint64_t set_pattern, int* num_wait_threads) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; - std::lock_guard lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - if (num_wait_threads) *num_wait_threads = ef->_waiters; - ef->_pattern = set_pattern; - ++ef->_cancelGeneration; - ef->_changed.NotifyAll(); + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; + KernelEventFlagPrivate* flag = entry->flag.get(); + std::lock_guard lock(flag->_mutex); + if (flag->_deleted) { + if (!entry->opened) return SCE_KERNEL_ERROR_ESRCH; + if (num_wait_threads) *num_wait_threads = 0; + return SCE_OK; + } + if (num_wait_threads) *num_wait_threads = flag->_waiters; + flag->_pattern = set_pattern; + ++flag->_cancelGeneration; + flag->_changed.NotifyAll(); return SCE_OK; } int APS5_VABI sceKernelPollEventFlag(KernelEventFlag ef, uint64_t bit_pattern, uint32_t wait_mode, uint64_t* result_pat) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; if (bit_pattern == 0 || !IsValidWaitMode(wait_mode)) return SCE_KERNEL_ERROR_EINVAL; - std::lock_guard lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - if (!ef->_multipleWaiters && ef->_waiters != 0) { + KernelEventFlagPrivate* flag = entry->flag.get(); + std::lock_guard lock(flag->_mutex); + if (flag->_deleted) { + if (!entry->opened) return SCE_KERNEL_ERROR_ESRCH; + if (result_pat) *result_pat = 0; + return SCE_KERNEL_ERROR_EACCES; + } + if (!flag->_multipleWaiters && flag->_waiters != 0) { if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_EPERM; } - if (!IsSatisfied(ef->_pattern, bit_pattern, wait_mode)) { - if (result_pat) *result_pat = ef->_pattern; + if (!IsSatisfied(flag->_pattern, bit_pattern, wait_mode)) { + if (result_pat) *result_pat = flag->_pattern; return SCE_KERNEL_ERROR_EBUSY; } - Consume(ef, bit_pattern, wait_mode, result_pat); + Consume(flag, bit_pattern, wait_mode, result_pat); return SCE_OK; } int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, uint32_t wait_mode, uint64_t* result_pat, KernelUseconds* timeout) { - if (!ef) return SCE_KERNEL_ERROR_ESRCH; - const auto keepAlive = ResolveEventFlag(ef); - if (!keepAlive) return SCE_KERNEL_ERROR_ESRCH; + const auto entry = ResolveEventFlag(ef); + if (!entry) return SCE_KERNEL_ERROR_ESRCH; if (bit_pattern == 0 || !IsValidWaitMode(wait_mode)) return SCE_KERNEL_ERROR_EINVAL; - std::unique_lock lock(ef->_mutex); - if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; - if (!ef->_multipleWaiters && ef->_waiters != 0) { + KernelEventFlagPrivate* flag = entry->flag.get(); + std::unique_lock lock(flag->_mutex); + if (flag->_deleted) { + if (!entry->opened) return SCE_KERNEL_ERROR_ESRCH; + if (result_pat) *result_pat = 0; + return SCE_KERNEL_ERROR_EACCES; + } + if (!flag->_multipleWaiters && flag->_waiters != 0) { if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_EPERM; } - if (IsSatisfied(ef->_pattern, bit_pattern, wait_mode)) { - Consume(ef, bit_pattern, wait_mode, result_pat); + if (IsSatisfied(flag->_pattern, bit_pattern, wait_mode)) { + Consume(flag, bit_pattern, wait_mode, result_pat); return SCE_OK; } - const uint64_t generation = ef->_cancelGeneration; - if (TraceSync()) std::fprintf(stderr, "[evf] wait %p '%s' want=0x%llx mode=0x%x have=0x%llx timeout=%d caller=%p outer=%p\n", static_cast(ef), ef->_name.c_str(), static_cast(bit_pattern), wait_mode, static_cast(ef->_pattern), timeout ? static_cast(*timeout) : -1, __builtin_return_address(0), static_cast(static_cast(__builtin_frame_address(0))[0])[1]); - ++ef->_waiters; + const uint64_t generation = flag->_cancelGeneration; + if (TraceSync()) std::fprintf(stderr, "[evf] wait %p '%s' want=0x%llx mode=0x%x have=0x%llx timeout=%d caller=%p outer=%p\n", static_cast(ef), flag->_name.c_str(), static_cast(bit_pattern), wait_mode, static_cast(flag->_pattern), timeout ? static_cast(*timeout) : -1, __builtin_return_address(0), static_cast(static_cast(__builtin_frame_address(0))[0])[1]); + ++flag->_waiters; WaiterNode waiter; waiter.bitPattern = bit_pattern; waiter.waitMode = wait_mode; - ef->pushWaiter(&waiter); + flag->pushWaiter(&waiter); struct WaiterGuard { KernelEventFlagPrivate* flag; WaiterNode* node; @@ -272,28 +326,28 @@ int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u --flag->_waiters; flag->_changed.NotifyAll(); } - } waiterGuard{ef, &waiter}; - const auto released = [&] { return ef->_deleted || ef->_cancelGeneration != generation || waiter.satisfied; }; + } waiterGuard{flag, &waiter}; + const auto released = [&] { return flag->_deleted || flag->_cancelGeneration != generation || waiter.satisfied; }; bool timedOut = false; const auto waitStart = std::chrono::steady_clock::now(); if (timeout) { const std::uint64_t deadline = TimedWait::DeadlineNanos(*timeout); - timedOut = !ef->_changed.WaitUntil(lock, deadline, released); + timedOut = !flag->_changed.WaitUntil(lock, deadline, released); *timeout = static_cast(TimedWait::RemainingMicros(deadline)); } else { - ef->_changed.Wait(lock, released); + flag->_changed.Wait(lock, released); } KernelTraceWait_nid_postfix("evf", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); - if (ef->_deleted) { + if (flag->_deleted) { if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_EACCES; } - if (ef->_cancelGeneration != generation) { + if (flag->_cancelGeneration != generation) { if (result_pat) *result_pat = 0; return SCE_KERNEL_ERROR_ECANCELED; } if (timedOut && !waiter.satisfied) { - if (result_pat) *result_pat = ef->_pattern; + if (result_pat) *result_pat = flag->_pattern; return SCE_KERNEL_ERROR_ETIMEDOUT; } if (result_pat) *result_pat = waiter.resultPattern; @@ -303,8 +357,8 @@ int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u } extern "C" int APS5_Test_GetEventFlagWaiterCount(KernelEventFlag handle) { - const auto keepAlive = ResolveEventFlag(handle); - if (!keepAlive) return -1; - std::lock_guard lock(keepAlive->_mutex); - return keepAlive->_waiters; + const auto entry = ResolveEventFlag(handle); + if (!entry) return -1; + std::lock_guard lock(entry->flag->_mutex); + return entry->flag->_waiters; } diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..08a5b55ada 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -688,11 +688,6 @@ int APS5_VABI sceKernelCloseEport(void) { return 0; } -int APS5_VABI sceKernelCloseEventFlag(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelCloseSema(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index efb2403c29..a635a36c7f 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -35,6 +35,7 @@ int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, std::uint32_t attr, std::uint64_t init_pattern, const void* param); int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef); int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name); +int APS5_VABI sceKernelCloseEventFlag(KernelEventFlag ef); int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); int APS5_VABI sceKernelClearEventFlag(KernelEventFlag ef, std::uint64_t bit_pattern); int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, std::uint64_t set_pattern, int* num_wait_threads); @@ -54,9 +55,11 @@ static constexpr int SCE_KERNEL_ERROR_EACCES = static_cast(0x8002000D); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); +static constexpr int SCE_KERNEL_ERROR_ENAMETOOLONG = static_cast(0x8002003F); static constexpr int SCE_KERNEL_ERROR_ECANCELED = static_cast(0x80020055); static constexpr std::uint32_t EVF_ATTR_SINGLE = 0x10; static constexpr std::uint32_t EVF_ATTR_MULTI = 0x20; +static constexpr std::uint32_t EVF_ATTR_SHARED = 0x100; static constexpr std::uint32_t EVF_WAITMODE_AND = 0x01; static constexpr std::uint32_t EVF_WAITMODE_OR = 0x02; static constexpr std::uint32_t EVF_WAITMODE_CLEAR_ALL = 0x10; @@ -212,18 +215,86 @@ int main() { Require(sceKernelWaitEventFlag(nullptr, 1, EVF_WAITMODE_OR, nullptr, nullptr) == SCE_KERNEL_ERROR_ESRCH); Require(sceKernelCancelEventFlag(nullptr, 0, nullptr) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelCreateEventFlag(&ef, "", EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelCreateEventFlag(&ef, "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn", EVF_ATTR_SHARED | EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + for (const std::uint32_t attr : {EVF_ATTR_MULTI, EVF_ATTR_SHARED | EVF_ATTR_MULTI}) { + Require(sceKernelCreateEventFlag(&ef, "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn", attr, 0, nullptr) == SCE_KERNEL_ERROR_ENAMETOOLONG); + } + KernelEventFlag opened = nullptr; Require(sceKernelOpenEventFlag(nullptr, "test") == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelOpenEventFlag(&opened, nullptr) == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelOpenEventFlag(&opened, "SceShellCoreUtilAppFocus") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); - Require(sceKernelCreateEventFlag(&ef, "test_open", EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); - Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_OK && opened == ef); + Require(sceKernelOpenEventFlag(&opened, "SceShellCoreUtilAppFocus") == SCE_KERNEL_ERROR_ESRCH && opened == nullptr); + Require(sceKernelCloseEventFlag(nullptr) == SCE_KERNEL_ERROR_ESRCH); + + KernelEventFlag plain = nullptr; + Require(sceKernelCreateEventFlag(&plain, "test_open", EVF_ATTR_MULTI, 0x01, nullptr) == SCE_OK); + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_KERNEL_ERROR_ESRCH && opened == nullptr); + Require(sceKernelCloseEventFlag(plain) == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelSetEventFlag(plain, 0x02) == SCE_OK); + KernelEventFlag plainTwin = nullptr; + Require(sceKernelCreateEventFlag(&plainTwin, "test_open", EVF_ATTR_MULTI, 0, nullptr) == SCE_OK); + Require(sceKernelDeleteEventFlag(plainTwin) == SCE_OK); + + Require(sceKernelCreateEventFlag(&ef, "test_open", EVF_ATTR_SHARED | EVF_ATTR_MULTI, 0x05, nullptr) == SCE_OK); + KernelEventFlag sharedTwin = nullptr; + Require(sceKernelCreateEventFlag(&sharedTwin, "test_open", EVF_ATTR_SHARED | EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EPERM && sharedTwin == nullptr); + Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_OK && opened != nullptr && opened != ef && opened != plain); + KernelEventFlag openedTwice = nullptr; + Require(sceKernelOpenEventFlag(&openedTwice, "test_open") == SCE_OK && openedTwice != opened && openedTwice != ef); Require(sceKernelSetEventFlag(opened, 0x08) == SCE_OK); + Require(sceKernelClearEventFlag(openedTwice, ~std::uint64_t{0x04}) == SCE_OK); std::uint64_t openedPattern = 0; - Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_OR, &openedPattern) == SCE_OK && openedPattern == 0x0d); + Require(sceKernelPollEventFlag(ef, 0x08, EVF_WAITMODE_OR, &openedPattern) == SCE_OK && openedPattern == 0x09); + Require(sceKernelDeleteEventFlag(opened) == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelCloseEventFlag(ef) == SCE_KERNEL_ERROR_EPERM); + Require(sceKernelCloseEventFlag(openedTwice) == SCE_OK); + Require(sceKernelCloseEventFlag(openedTwice) == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelSetEventFlag(openedTwice, 0x01) == SCE_KERNEL_ERROR_ESRCH); + + std::atomic orphanWaitRes{-1}; + std::uint64_t orphanWaitPat = 0xdeadbeef; + std::thread orphanWaiter([&] { + orphanWaitRes = sceKernelWaitEventFlag(opened, 0x80, EVF_WAITMODE_OR, &orphanWaitPat, nullptr); + }); + WaitForEventFlagWaiters(opened, 1); Require(sceKernelDeleteEventFlag(ef) == SCE_OK); - opened = nullptr; - Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_KERNEL_ERROR_ENOENT && opened == nullptr); + orphanWaiter.join(); + Require(orphanWaitRes == SCE_KERNEL_ERROR_EACCES && orphanWaitPat == 0); + KernelEventFlag reopened = nullptr; + Require(sceKernelOpenEventFlag(&reopened, "test_open") == SCE_KERNEL_ERROR_ESRCH && reopened == nullptr); + std::uint64_t orphanPattern = 0xdeadbeef; + Require(sceKernelPollEventFlag(opened, 0, EVF_WAITMODE_OR, &orphanPattern) == SCE_KERNEL_ERROR_EINVAL && orphanPattern == 0xdeadbeef); + Require(sceKernelPollEventFlag(opened, 0x01, EVF_WAITMODE_OR, &orphanPattern) == SCE_KERNEL_ERROR_EACCES && orphanPattern == 0); + orphanPattern = 0xdeadbeef; + KernelUseconds orphanTimeout = 1000; + Require(sceKernelWaitEventFlag(opened, 0x01, EVF_WAITMODE_OR, &orphanPattern, &orphanTimeout) == SCE_KERNEL_ERROR_EACCES && orphanPattern == 0 && orphanTimeout == 1000); + Require(sceKernelSetEventFlag(opened, 0x02) == SCE_OK); + Require(sceKernelClearEventFlag(opened, 0) == SCE_OK); + int orphanCount = -1; + Require(sceKernelCancelEventFlag(opened, 0x04, &orphanCount) == SCE_OK && orphanCount == 0); + Require(sceKernelDeleteEventFlag(opened) == SCE_KERNEL_ERROR_EPERM); + + Require(sceKernelCreateEventFlag(&ef, "test_open", EVF_ATTR_SHARED | EVF_ATTR_SINGLE, 0x01, nullptr) == SCE_OK); + Require(sceKernelOpenEventFlag(&reopened, "test_open") == SCE_OK && reopened != opened); + Require(sceKernelCloseEventFlag(opened) == SCE_OK); + Require(sceKernelCloseEventFlag(opened) == SCE_KERNEL_ERROR_ESRCH); + std::atomic sharedSingleRes{-1}; + std::uint64_t sharedSinglePat = 0; + std::thread sharedSingleWaiter([&] { + sharedSingleRes = sceKernelWaitEventFlag(ef, 0x80, EVF_WAITMODE_OR, &sharedSinglePat, nullptr); + }); + WaitForEventFlagWaiters(ef, 1); + openedPattern = 0xdeadbeef; + Require(sceKernelPollEventFlag(reopened, 0x01, EVF_WAITMODE_OR, &openedPattern) == SCE_KERNEL_ERROR_EPERM && openedPattern == 0); + Require(sceKernelSetEventFlag(reopened, 0x80) == SCE_OK); + sharedSingleWaiter.join(); + Require(sharedSingleRes == SCE_OK && sharedSinglePat == 0x81); + Require(sceKernelCloseEventFlag(reopened) == SCE_OK); + Require(sceKernelDeleteEventFlag(ef) == SCE_OK); + Require(sceKernelDeleteEventFlag(plain) == SCE_OK); Require(sceKernelCreateEventFlag(&ef, "flag", EVF_ATTR_MULTI, 0x15, nullptr) == SCE_OK); std::uint64_t pattern = 0; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index a8dc3a9fc1..6cdba1cc80 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -59,8 +59,7 @@ - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console -- [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - attribute bit 0x100 is accepted by firmware 13.60 alongside 0x1, 0x2, 0x10 and 0x20, but what it changes is unknown, so a flag created with it calls `NotImplemented_nid_no_patch`. -- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - returns the handle of an undeleted event flag this process created with the same name, and `SCE_KERNEL_ERROR_ENOENT` otherwise. Flags owned by other processes, such as the shell's `SceShellCoreUtilAppFocus`, do not exist here, and which attribute makes a flag openable on the console is not modelled. The error code for a missing name and the choice between flags sharing a name are unverified. +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) From 3e0feb4bef5d9b9d0c6af62730786954ef6ee1fc Mon Sep 17 00:00:00 2001 From: akkadbakkad89 Date: Wed, 21 Oct 2026 19:07:15 +0530 Subject: [PATCH 130/318] feat(libc): export _Thrd_yield --- core/libs/prx/libc/src/Threads.cpp | 4 ++++ core/libs/tests/GuestLibcThreads.cpp | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index a00ba39463..1e7ea35688 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -379,6 +379,10 @@ int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { } } +int APS5_VABI _Thrd_yield_nid_postfix() { + std::this_thread::yield(); + return threadSuccess; +} int APS5_VABI _Cnd_signal_nid_postfix(void** handle) { return SignalCondition_nid_no_patch(handle); diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 001558be39..e811e6f77d 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -55,6 +55,7 @@ int APS5_VABI _Thrd_equal_nid_postfix(void* first, void* second); void* APS5_VABI _Thrd_current_nid_postfix(void); int APS5_VABI _Mtx_current_owns_nid_postfix(void** handle); void* APS5_VABI scePthreadSelf(); +int APS5_VABI _Thrd_yield_nid_postfix(); } static void Require(bool value) { if (!value) std::abort(); } @@ -374,6 +375,14 @@ static void ForwardedCalls() { RequireThrows([] { _Tss_get_nid_postfix(0); }); } +static void ThreadHelpers() { + Require(_Thrd_yield_nid_postfix() == success); + std::thread other([&] { + Require(_Thrd_yield_nid_postfix() == success); + }); + other.join(); +} + int main() { KernelKeys(); ForwardedCalls(); @@ -384,4 +393,5 @@ int main() { ConditionSignal(); PadLaunchAndJoin(); PadDetach(); + ThreadHelpers(); } From 038d9a97d77ea4e60575348f7bac5e38f9fc2983 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 11:06:19 -0300 Subject: [PATCH 131/318] fix(libSceAgcDriver): return the console's workload stream registration errors Measured on firmware 13.60: registering an id outside 1 to 31 or one already registered returns 0x8a6c0033, checked before a null stream, which returns 0x8a6c0035; unregistering an id outside 1 to 31 returns 0x8a6c0033 and one that is not registered 0x8a6c003a. Co-Authored-By: Claude Opus 5.5 --- .../Resource/src/Registration.cpp | 20 +++++++++++-------- core/libs/tests/GuestAgcDriverResources.cpp | 17 ++++++++++++++++ docs/dev/debt/libSceAgc.md | 2 +- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index f3fbd0ba67..7cae8c3afb 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -9,6 +9,9 @@ // Resource registration feeds GPU debugging tools, none of which are attached; the game treats this code as benign. static constexpr int SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE = static_cast(0x8A6C9018); static constexpr uint32_t RESOURCE_REGISTRATION_MAX_NAME_LENGTH = 0xfc; +static constexpr int SCE_AGC_ERROR_WORKLOAD_STREAM_INVALID_ID = static_cast(0x8A6C0033); +static constexpr int SCE_AGC_ERROR_WORKLOAD_STREAM_NULL = static_cast(0x8A6C0035); +static constexpr int SCE_AGC_ERROR_WORKLOAD_STREAM_NOT_REGISTERED = static_cast(0x8A6C003A); static constexpr uint32_t WORKLOAD_STREAM_MIN_ID = 1; static constexpr uint32_t WORKLOAD_STREAM_MAX_ID = 31; @@ -49,10 +52,11 @@ int APS5_VABI sceAgcDriverRegisterMultipleResources(uint32_t* resourceHandle, ui } int APS5_VABI sceAgcDriverRegisterWorkloadStream(uint32_t stream_id, const void* stream) { - if (stream != nullptr && stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) { - std::lock_guard lock(g_workloadStreamLock); - g_workloadStreams |= 1u << stream_id; - } + if (stream_id < WORKLOAD_STREAM_MIN_ID || stream_id > WORKLOAD_STREAM_MAX_ID) return SCE_AGC_ERROR_WORKLOAD_STREAM_INVALID_ID; + std::lock_guard lock(g_workloadStreamLock); + if ((g_workloadStreams & (1u << stream_id)) != 0) return SCE_AGC_ERROR_WORKLOAD_STREAM_INVALID_ID; + if (stream == nullptr) return SCE_AGC_ERROR_WORKLOAD_STREAM_NULL; + g_workloadStreams |= 1u << stream_id; return 0; } @@ -83,10 +87,10 @@ int APS5_VABI sceAgcDriverUnregisterAllResourcesForOwner(uint32_t owner_handle) } int APS5_VABI sceAgcDriverUnregisterWorkloadStream(uint32_t stream_id) { - if (stream_id >= WORKLOAD_STREAM_MIN_ID && stream_id <= WORKLOAD_STREAM_MAX_ID) { - std::lock_guard lock(g_workloadStreamLock); - g_workloadStreams &= ~(1u << stream_id); - } + if (stream_id < WORKLOAD_STREAM_MIN_ID || stream_id > WORKLOAD_STREAM_MAX_ID) return SCE_AGC_ERROR_WORKLOAD_STREAM_INVALID_ID; + std::lock_guard lock(g_workloadStreamLock); + if ((g_workloadStreams & (1u << stream_id)) == 0) return SCE_AGC_ERROR_WORKLOAD_STREAM_NOT_REGISTERED; + g_workloadStreams &= ~(1u << stream_id); return 0; } diff --git a/core/libs/tests/GuestAgcDriverResources.cpp b/core/libs/tests/GuestAgcDriverResources.cpp index fc55b15a8a..5ee342a763 100644 --- a/core/libs/tests/GuestAgcDriverResources.cpp +++ b/core/libs/tests/GuestAgcDriverResources.cpp @@ -92,8 +92,25 @@ int main() { Require(sceAgcDriverFindResourcesPublic(nullptr, nullptr) == Unavailable); Require(sceAgcDriverUnregisterAllResourcesForOwner(owner) == Unavailable); Require(std::string_view(name) == "unchanged"); + constexpr int InvalidStream = static_cast(0x8A6C0033); + constexpr int NullStream = static_cast(0x8A6C0035); + constexpr int StreamNotRegistered = static_cast(0x8A6C003A); + for (const std::uint32_t id : {0u, 32u, 0x101u, 0xffffffffu}) { + Require(sceAgcDriverRegisterWorkloadStream(id, memory) == InvalidStream); + Require(sceAgcDriverRegisterWorkloadStream(id, nullptr) == InvalidStream); + Require(sceAgcDriverUnregisterWorkloadStream(id) == InvalidStream); + } + Require(sceAgcDriverRegisterWorkloadStream(2u, nullptr) == NullStream); + Require(sceAgcDriverUnregisterWorkloadStream(2u) == StreamNotRegistered); + Require(sceAgcDriverRegisterWorkloadStream(1u, memory) == 0); + Require(sceAgcDriverRegisterWorkloadStream(1u, memory) == InvalidStream); + Require(sceAgcDriverRegisterWorkloadStream(1u, nullptr) == InvalidStream); + Require(sceAgcDriverRegisterWorkloadStream(31u, reinterpret_cast(memory) + 1) == 0); + Require(sceAgcDriverUnregisterWorkloadStream(1u) == 0); + Require(sceAgcDriverUnregisterWorkloadStream(1u) == StreamNotRegistered); Require(sceAgcDriverRegisterWorkloadStream(1u, memory) == 0); Require(sceAgcDriverUnregisterWorkloadStream(1u) == 0); + Require(sceAgcDriverUnregisterWorkloadStream(31u) == 0); Require(!sceAgcDriverIsCaptureInProgress()); Require(!sceAgcDriverIsTraceInProgress()); Require(!sceAgcDriverIsSubmitValidationEnabled()); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 0bd4347123..96d78d4983 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -45,7 +45,7 @@ - [sceAgcDebugRaiseException](../../../core/libs/prx/libSceAgc/Misc/src/Debug.cpp) (libSceAgc) - logs the exception id and returns 0, as retail hardware does nothing per [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c); the trap (`ud2`) OpenAGC describes for development hardware is not reproduced. Neither is verified on a console. The `uint32` exception id is [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/6f0cddd4404ec5203e71fea77e75fc373299ba3f/src/libs/agc.cpp)'s claim; OpenAGC declares no arguments, so the logged id may be leftover register contents - [sceAgcSetAmmSemaphoreMemory, sceAgcGetSemaphoreLabel](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) (libSceAgc) - the `(memory, sizeInBytes)` and `(index, label)` signatures, the 16 KiB alignment of the memory and its size, the single registration, the 32-byte labels and the zeroed first 8 bytes of each are what [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L508-L546) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Agc/AgcExports.AmmSemaphore.cs) agree on; KytyPS5 has neither function. OpenAGC clears the whole memory and sharpemu only those 8 bytes, so a registration whose labels hold data past them throws. The two return different errors for every rejected call (OpenAGC `0x8a6c0002`, `0x8a6c000b`, `0x8a6c0048` and `0x8a6c0049`, sharpemu generic invalid-argument and already-exists errors), so every rejected call throws, including a lookup before registration. Not measured on the console; PPSA02801 imports both (OpenAGC's import analysis), no title is shown calling them. -- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. +- [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) - the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From 3e1b0f5aa41a7f7745c1930335279787aabcdf3b Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 20 Oct 2026 22:34:45 +0200 Subject: [PATCH 132/318] feat(libSceLibcInternal): export __inet_ntoa_r The PS5 3.20 export inventory of libSceLibcInternal lists __inet_ntoa_r next to inet_ntoa_r, and no AnyPS5 library exported it, so a title importing it could not resolve it. It now runs inet_ntoa_r: FreeBSD 11.4's arpa/inet.h names the function __inet_ntoa_r, and inet_ntoa.c makes inet_ntoa_r a weak alias of it. __inet_addr, __inet_aton and __inet_ntoa are in the same inventory, but libc already exports them and the relinker also looks up libSceLibcInternal imports in libc, so they are not added here. guest_inet_address formats an address through __inet_ntoa_r into a buffer of exactly the text's size, and checks that a buffer one byte short is returned unchanged with ENOSPC and that inet_ntoa's buffer is left alone. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibcInternal/Inet.cpp | 4 ++++ core/libs/tests/GuestInetAddress.cpp | 11 +++++++++++ docs/dev/debt/libSceLibcInternal.md | 1 + 3 files changed, 16 insertions(+) diff --git a/core/libs/prx/libSceLibcInternal/Inet.cpp b/core/libs/prx/libSceLibcInternal/Inet.cpp index 90996aacf6..c11c6201ac 100644 --- a/core/libs/prx/libSceLibcInternal/Inet.cpp +++ b/core/libs/prx/libSceLibcInternal/Inet.cpp @@ -97,6 +97,10 @@ char* APS5_VABI inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std return buffer; } +char* APS5_VABI __inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std::uint32_t size) { + return inet_ntoa_r_nid_postfix(address, buffer, size); +} + char* APS5_VABI inet_ntoa_nid_postfix(std::uint32_t address) { std::memcpy(g_ntoaBuffer, "[inet_ntoa error]", sizeof(g_ntoaBuffer)); __inet_ntop_nid_postfix(AddressFamilyInet, &address, g_ntoaBuffer, sizeof(g_ntoaBuffer)); diff --git a/core/libs/tests/GuestInetAddress.cpp b/core/libs/tests/GuestInetAddress.cpp index 80447cad5d..ad2275caee 100644 --- a/core/libs/tests/GuestInetAddress.cpp +++ b/core/libs/tests/GuestInetAddress.cpp @@ -10,6 +10,8 @@ int APS5_VABI inet_aton_nid_postfix(const char* text, std::uint32_t* address); std::uint32_t APS5_VABI inet_addr_nid_postfix(const char* text); char* APS5_VABI inet_ntoa_nid_postfix(std::uint32_t address); char* APS5_VABI inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std::uint32_t size); +char* APS5_VABI __inet_ntoa_r_nid_postfix(std::uint32_t address, char* buffer, std::uint32_t size); +int* APS5_VABI __error_nid_postfix(); } namespace { @@ -64,5 +66,14 @@ int main() { Require(inet_ntoa_r_nid_postfix(Network(1, 2, 3, 4), buffer, sizeof(buffer)) == buffer && std::string(buffer) == "1.2.3.4", "inet_ntoa_r"); char tiny[4] = {'k', 'e', 'e', 'p'}; Require(inet_ntoa_r_nid_postfix(Network(1, 2, 3, 4), tiny, sizeof(tiny)) == tiny, "inet_ntoa_r small buffer returns it"); + + char exact[14]; + std::memset(exact, 'z', sizeof(exact)); + Require(__inet_ntoa_r_nid_postfix(Network(192, 168, 0, 255), exact, sizeof(exact)) == exact && std::string(exact) == "192.168.0.255", "__inet_ntoa_r"); + char oneShort[13]; + std::memset(oneShort, 'z', sizeof(oneShort)); + *__error_nid_postfix() = 0; + Require(__inet_ntoa_r_nid_postfix(Network(192, 168, 0, 255), oneShort, sizeof(oneShort)) == oneShort && *__error_nid_postfix() == 28, "__inet_ntoa_r small buffer"); + Require(std::string(oneShort, sizeof(oneShort)) == std::string(sizeof(oneShort), 'z') && std::string(text) == "255.255.255.255", "__inet_ntoa_r leaves the buffers"); return 0; } diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index ff8c271f1f..35343d02c3 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -17,3 +17,4 @@ - [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read - [inet_aton, inet_addr, inet_ntoa and inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) follow the FreeBSD code in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8ADE0 to 0x8B3D0) and matched it on a PS5 (firmware 13.60) for 20 `inet_aton` inputs; a null text, which the console dereferences, throws. `inet_ntoa_r` formats through libkernel's `__inet_ntop`, so a null buffer with room for the text throws and a null buffer that is too small sets `ENOSPC`, as `__inet_ntop` does for a null destination; the console's `inet_ntoa_r` was not measured with a null buffer +- [__inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) was not measured on the console: it runs `inet_ntoa_r`, as FreeBSD 11.4 names the function `__inet_ntoa_r` ([arpa/inet.h](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/include/arpa/inet.h#L129)) and makes `inet_ntoa_r` a weak alias of it ([inet_ntoa.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/inet/inet_ntoa.c#L62-L76)) From 22cfc4b0bbdb86bcf6176add276a4c3bcd366d79 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:57:36 +0530 Subject: [PATCH 133/318] fix(libSceSaveDataDialog.native): initialize result directory name on empty selection When sceSaveDataDialogGetResult is passed a non-null dir_name pointer but no directory name was selected by the dialog, write the empty string into dir_name so the caller's output buffer is properly null-terminated instead of retaining uninitialized bytes. --- .../prx/libSceSaveDataDialog.native/Export.cpp | 2 +- core/libs/tests/GuestSaveDataDialog.cpp | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp index 54c4fd0926..0a07c63d79 100644 --- a/core/libs/prx/libSceSaveDataDialog.native/Export.cpp +++ b/core/libs/prx/libSceSaveDataDialog.native/Export.cpp @@ -44,7 +44,7 @@ int APS5_VABI sceSaveDataDialogGetResult(void* result) { r->result = SAVE_DATA_DIALOG_RESULT_OK; r->button_id = SAVE_DATA_DIALOG_BUTTON_ID_OK; r->user_data = g_user_data; - if (r->dir_name != nullptr && g_dir_name[0] != '\0') { + if (r->dir_name != nullptr) { std::snprintf(static_cast(r->dir_name)->data, sizeof(SaveDataDirName::data), "%s", g_dir_name); } diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp index 1c3ffc702d..5cc72f7671 100644 --- a/core/libs/tests/GuestSaveDataDialog.cpp +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -106,6 +106,22 @@ int main() { Require(sceSaveDataDialogTerminate() == 0, "terminate"); Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate when already terminated"); Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); + + Require(sceSaveDataDialogInitialize() == 0, "re-initialize for empty result"); + param = {}; + param.size = sizeof(param); + param.mode = SAVE_DATA_DIALOG_MODE_PROGRESS_BAR; + Require(sceSaveDataDialogOpen(¶m) == 0, "open progress without items"); + Require(sceSaveDataDialogClose(nullptr) == 0, "close empty progress"); + Require(sceSaveDataDialogUpdateStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, "finish empty progress"); + SaveDataDirName emptySelected{}; + std::memset(emptySelected.data, 'X', sizeof(emptySelected.data)); + SaveDataDialogResult emptyResult{}; + emptyResult.dir_name = &emptySelected; + Require(sceSaveDataDialogGetResult(&emptyResult) == 0, "read empty result"); + Require(emptySelected.data[0] == '\0', "empty directory name must be null-terminated"); + Require(sceSaveDataDialogTerminate() == 0, "terminate empty progress"); + Require(sceSaveDataDialogProgressBarInc(0, 10) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress inc after terminate"); Require(sceSaveDataDialogProgressBarSetValue(0, 50) == SAVE_DATA_DIALOG_ERROR_NOT_RUNNING, "progress set value after terminate"); } From b32a5ee32b4490d069f305821af027cd5bd58a64 Mon Sep 17 00:00:00 2001 From: lakhantheboss <61951686+lakhantheboss@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:08:17 +0530 Subject: [PATCH 134/318] test(libSceSaveDataDialog.native): cover empty selection dialog result Verify that sceSaveDataDialogGetResult writes an empty null-terminated directory name string when a selection dialog finishes with no selected directory items. --- core/libs/tests/GuestSaveDataDialog.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/libs/tests/GuestSaveDataDialog.cpp b/core/libs/tests/GuestSaveDataDialog.cpp index 5cc72f7671..c520246469 100644 --- a/core/libs/tests/GuestSaveDataDialog.cpp +++ b/core/libs/tests/GuestSaveDataDialog.cpp @@ -107,6 +107,22 @@ int main() { Require(sceSaveDataDialogTerminate() == SAVE_DATA_DIALOG_ERROR_NOT_INITIALIZED, "terminate when already terminated"); Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_NONE, "terminated state"); + Require(sceSaveDataDialogInitialize() == 0, "re-initialize for empty selection"); + SaveDataDialogParam emptySelectionParam{}; + emptySelectionParam.size = sizeof(emptySelectionParam); + emptySelectionParam.mode = 1; + Require(sceSaveDataDialogOpen(&emptySelectionParam) == 0, "open selection dialog without items"); + Require(sceSaveDataDialogGetStatus() == SAVE_DATA_DIALOG_STATUS_FINISHED, "selection finishes immediately"); + SaveDataDirName emptySelectionDir{}; + std::memset(emptySelectionDir.data, 'X', sizeof(emptySelectionDir.data)); + SaveDataDialogResult emptySelectionResult{}; + emptySelectionResult.dir_name = &emptySelectionDir; + Require(sceSaveDataDialogGetResult(&emptySelectionResult) == 0, "read empty selection result"); + Require(emptySelectionDir.data[0] == '\0', "empty selection directory name must be null-terminated"); + Require(emptySelectionResult.mode == emptySelectionParam.mode && + emptySelectionResult.result == SAVE_DATA_DIALOG_RESULT_OK, "selection result status"); + Require(sceSaveDataDialogTerminate() == 0, "terminate selection"); + Require(sceSaveDataDialogInitialize() == 0, "re-initialize for empty result"); param = {}; param.size = sizeof(param); From c8ad92f8783612f8bf4431cb92ed66139cda3dac Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 20 Oct 2026 22:34:24 +0200 Subject: [PATCH 135/318] feat(libSceLibcInternal): export _err, _warn and _getprogname The PS5 3.20 export inventory lists _err, _warn and _getprogname in libSceLibcInternal next to err, warn and getprogname, with the NIDs UhKI6z9WWuo, XQFE8Y58WZM and iI6kGxgXzcU. In FreeBSD 11.4 libc the underscore names are the definitions and the public names are weak references to them (lib/libc/gen/err.c, lib/libc/gen/getprogname.c), and libthr interposes none of them, so each pair is one function. _err and _warn now hand their arguments to verr and vwarn, which use errno at the call as err and warn do; _getprogname returns getprogname(). The debt file records that the console's module was not read. guest_err checks the _warn and _err text for errno 13 with a formatted message, that _err passes its status to the err_set_exit hook, and that _warn without a format returns without calling the hook. guest_program_name checks that _getprogname returns the current __progname pointer. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibcInternal/Err.cpp | 13 +++++++++++++ core/libs/prx/libSceLibcInternal/Export.cpp | 4 ++++ core/libs/tests/GuestErr.cpp | 11 +++++++++++ core/libs/tests/GuestProgramName.cpp | 3 +++ docs/dev/debt/libSceLibcInternal.md | 1 + 5 files changed, 32 insertions(+) diff --git a/core/libs/prx/libSceLibcInternal/Err.cpp b/core/libs/prx/libSceLibcInternal/Err.cpp index f16dd24ca1..08f9ae7622 100644 --- a/core/libs/prx/libSceLibcInternal/Err.cpp +++ b/core/libs/prx/libSceLibcInternal/Err.cpp @@ -108,6 +108,13 @@ void APS5_VABI warn_nid_postfix(const char* format, ...) { APS5_ERR_VA_END(args); } +void APS5_VABI _warn_nid_postfix(const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + vwarn_nid_postfix(format, reinterpret_cast(args)); + APS5_ERR_VA_END(args); +} + void APS5_VABI warnx_nid_postfix(const char* format, ...) { APS5_ERR_VA_LIST args; APS5_ERR_VA_START(args, format); @@ -128,6 +135,12 @@ void APS5_VABI warnx_nid_postfix(const char* format, ...) { verrc_nid_postfix(status, code, format, reinterpret_cast(args)); } +[[noreturn]] void APS5_VABI _err_nid_postfix(int status, const char* format, ...) { + APS5_ERR_VA_LIST args; + APS5_ERR_VA_START(args, format); + verr_nid_postfix(status, format, reinterpret_cast(args)); +} + [[noreturn]] void APS5_VABI errx_nid_postfix(int status, const char* format, ...) { APS5_ERR_VA_LIST args; APS5_ERR_VA_START(args, format); diff --git a/core/libs/prx/libSceLibcInternal/Export.cpp b/core/libs/prx/libSceLibcInternal/Export.cpp index 7ca113639e..b8c3288715 100644 --- a/core/libs/prx/libSceLibcInternal/Export.cpp +++ b/core/libs/prx/libSceLibcInternal/Export.cpp @@ -109,6 +109,10 @@ const char* APS5_VABI getprogname_nid_postfix() { return __progname_nid_postfix; } +const char* APS5_VABI _getprogname_nid_postfix() { + return getprogname_nid_postfix(); +} + void APS5_VABI __cxa_finalize_nid_postfix(void* dsoHandle) { CxaFinalize_nid_no_patch(dsoHandle); } diff --git a/core/libs/tests/GuestErr.cpp b/core/libs/tests/GuestErr.cpp index cf7f656594..14c8772bc2 100644 --- a/core/libs/tests/GuestErr.cpp +++ b/core/libs/tests/GuestErr.cpp @@ -17,9 +17,11 @@ extern "C" { void APS5_VABI err_set_file_nid_postfix(FileStream* stream); void APS5_VABI err_set_exit_nid_postfix(ErrExit function); void APS5_VABI warn_nid_postfix(const char* format, ...); +void APS5_VABI _warn_nid_postfix(const char* format, ...); void APS5_VABI warnc_nid_postfix(int code, const char* format, ...); void APS5_VABI warnx_nid_postfix(const char* format, ...); [[noreturn]] void APS5_VABI err_nid_postfix(int status, const char* format, ...); +[[noreturn]] void APS5_VABI _err_nid_postfix(int status, const char* format, ...); [[noreturn]] void APS5_VABI errc_nid_postfix(int status, int code, const char* format, ...); [[noreturn]] void APS5_VABI errx_nid_postfix(int status, const char* format, ...); void APS5_VABI vwarn_nid_postfix(const char* format, VaList* args); @@ -148,6 +150,9 @@ int main() { *__error_nid_postfix() = 2; warn_nid_postfix(nullptr); Require(capture.Take() == "prog: " + noEntry + "\n", "warn without format"); + *__error_nid_postfix() = 13; + _warn_nid_postfix("open %s", "b.txt"); + Require(capture.Take() == "prog: open b.txt: " + denied + "\n", "_warn"); warnc_nid_postfix(13, "%d-%s", 7, "x"); Require(capture.Take() == "prog: 7-x: " + denied + "\n", "warnc"); warnx_nid_postfix("plain %c", 'q'); @@ -163,6 +168,12 @@ int main() { *__error_nid_postfix() = 2; Require(ExitStatus([] { err_nid_postfix(5, "e"); }) == 5, "err status"); Require(capture.Take() == "prog: e: " + noEntry + "\n", "err text"); + *__error_nid_postfix() = 13; + Require(ExitStatus([] { _err_nid_postfix(7, "%s %d", "f", 8); }) == 7 && g_hookStatus == 7, "_err status"); + Require(capture.Take() == "prog: f 8: " + denied + "\n", "_err text"); + *__error_nid_postfix() = 2; + Require(ExitStatus([] { _warn_nid_postfix(nullptr); }) == -1 && g_hookStatus == 7, "_warn does not exit"); + Require(capture.Take() == "prog: " + noEntry + "\n", "_warn without format"); *__error_nid_postfix() = 2; CallVariant(Variant::Warn, 0, 0, "open %s", "a.txt"); diff --git a/core/libs/tests/GuestProgramName.cpp b/core/libs/tests/GuestProgramName.cpp index 48be372cc8..7f14669adb 100644 --- a/core/libs/tests/GuestProgramName.cpp +++ b/core/libs/tests/GuestProgramName.cpp @@ -4,6 +4,7 @@ extern "C" { const char* APS5_VABI getprogname_nid_postfix(); +const char* APS5_VABI _getprogname_nid_postfix(); extern const char* __progname_nid_postfix; int* APS5_VABI __error_nid_postfix(); } @@ -14,12 +15,14 @@ int main() { const char* const original = __progname_nid_postfix; *__error_nid_postfix() = 45; Require(getprogname_nid_postfix() == original); + Require(_getprogname_nid_postfix() == original); Require(std::strcmp(getprogname_nid_postfix(), "eboot.bin") == 0); Require(*__error_nid_postfix() == 45); char renamed[] = "quake2"; __progname_nid_postfix = renamed; Require(getprogname_nid_postfix() == renamed); + Require(_getprogname_nid_postfix() == renamed); renamed[0] = 'Q'; Require(std::strcmp(getprogname_nid_postfix(), "Quake2") == 0); diff --git a/docs/dev/debt/libSceLibcInternal.md b/docs/dev/debt/libSceLibcInternal.md index ff8c271f1f..d61e6d1e18 100644 --- a/docs/dev/debt/libSceLibcInternal.md +++ b/docs/dev/debt/libSceLibcInternal.md @@ -16,4 +16,5 @@ - [getopt, getopt_long](../../../core/libs/prx/libSceLibcInternal/Getopt.cpp) (libSceLibcInternal) follow FreeBSD 11 [getopt.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt.c) and [getopt_long.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/getopt_long.c) with `GNU_COMPATIBLE`: `getopt` does not permute, `getopt_long` permutes unless `POSIXLY_CORRECT` is set in the guest environment or the option string starts with `+`, and the exported `opterr`, `optind`, `optopt`, `optreset` and `optarg` start at 1, 1, 0, 0 and null. Warnings go to the guest's current `stderr` prefixed with `__progname`, where FreeBSD's `warnx` keeps the `stderr` of its first call. A null `getopt` option string, a null argument vector or argument, a negative `optind` and an `optind` moved inside a pending permutation throw where FreeBSD faults. `getopt_long_only` is not exported. That the console's `libSceLibcInternal` uses this FreeBSD code is unverified - [posix_spawnattr_getflags, getpgroup, getschedparam, getschedpolicy, getsigdefault, getsigmask, setpgroup, setschedparam, setschedpolicy and setsigmask](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp) (libSceLibcInternal) read and write the 0x30-byte attribute object at the offsets of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8D860 to 0x8D910: flags at 0, process group at 4, scheduling parameter at 8, policy at 0xC, default and masked signal sets at 0x10 and 0x20), which never fail there and return 0; the layout matched on a PS5 (firmware 13.60). Uninitialized attributes and null pointers, which the console dereferences, throw as the existing setters do - [sys_siglist and sys_nsig](../../../core/libs/prx/libSceLibcInternal/SignalList.cpp) (libSceLibcInternal) hold the 32 descriptions and the count of [FreeBSD 11.4](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/siglist.c#L73-L107) `siglist.c` (`NSIG` 32, signal 0 to `SIGUSR2`); the console's table was not read +- [_err, _warn](../../../core/libs/prx/libSceLibcInternal/Err.cpp) and [_getprogname](../../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) are assumed to be the same functions as `err`, `warn` and `getprogname` and run their code; the console's module was not read. FreeBSD 11.4 defines the underscore names and makes the public names weak references to them ([`_err`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/err.c#L70-L79), [`_warn`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/err.c#L135-L144), [`_getprogname`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/getprogname.c#L10-L17)) - [inet_aton, inet_addr, inet_ntoa and inet_ntoa_r](../../../core/libs/prx/libSceLibcInternal/Inet.cpp) (libSceLibcInternal) follow the FreeBSD code in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x8ADE0 to 0x8B3D0) and matched it on a PS5 (firmware 13.60) for 20 `inet_aton` inputs; a null text, which the console dereferences, throws. `inet_ntoa_r` formats through libkernel's `__inet_ntop`, so a null buffer with room for the text throws and a null buffer that is too small sets `ENOSPC`, as `__inet_ntop` does for a null destination; the console's `inet_ntoa_r` was not measured with a null buffer From cbdbc601dc526a10d474eba1d486a17b9a2b60ce Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:11:13 +0300 Subject: [PATCH 136/318] fix(shader): keep undecoded OpExtInst and runtime logical guards in specialization --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 11 --------- .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 8 +++---- .../SpirvBackend/src/SpirvSpecialization.cpp | 24 ++----------------- 3 files changed, 6 insertions(+), 37 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index d78cb62a9f..00796cd2b5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -322,8 +322,6 @@ void testPrivateCompositeTableLoadFolding() { const auto input = module.DefineGlobalVariable(typeInputPtr, spv::StorageClassInput); const auto table = module.DefineInitializedGlobalVariable(typeTablePtr, spv::StorageClassPrivate, initial); const auto output = module.DefineGlobalVariable(typePrivatePtr, spv::StorageClassPrivate); - const auto glsl = module.Import("GLSL.std.450"); - const auto customSet = module.Import("NonSemantic.Test"); const auto mainId = module.AllocateId(); const auto lane = module.AllocateId(); const auto rawCond = module.AllocateId(); @@ -341,11 +339,6 @@ void testPrivateCompositeTableLoadFolding() { module.AddFunction(accessOp, typePrivatePtr, writePtr, table, zero); module.AddFunction(spv::OpStore, writePtr, lane); } - const auto liveGlsl = module.AllocateId(); - module.AddFunction(spv::OpExtInst, typeU32, module.AllocateId(), glsl, 37u, lane, zero); - module.AddFunction(spv::OpExtInst, typeU32, liveGlsl, glsl, 37u, lane, one); - module.AddFunction(spv::OpStore, output, liveGlsl); - module.AddFunction(spv::OpExtInst, typeU32, module.AllocateId(), customSet, 1u, lane); module.AddFunction(spv::OpUGreaterThan, typeBool, rawCond, lane, zero); module.AddFunction(spv::OpLogicalOr, typeBool, cond, rawCond, falseVal); module.AddFunction(spv::OpIAdd, typeU32, baseIndex, one, one); @@ -364,16 +357,12 @@ void testPrivateCompositeTableLoadFolding() { check(!foldedCounts.contains(spv::OpLogicalOr), "a right-false OpLogicalOr was not simplified"); check(!foldedCounts.contains(spv::OpSelect), "an identical-arm OpSelect was not simplified"); check(foldedCounts.contains(spv::OpLoad) && foldedCounts.at(spv::OpLoad) == 1u, "a constant-index private composite table load was not folded"); - check(foldedCounts.contains(spv::OpExtInst) && foldedCounts.at(spv::OpExtInst) == 2u, "an unused GLSL.std.450 OpExtInst was not removed or a live/non-GLSL.std.450 OpExtInst was removed"); bool addedThirty = false; - bool keptCustomExtInst = false; for (std::size_t cursor = 5; cursor < folded.size(); cursor += folded[cursor] >> 16u) { if ((folded[cursor] >> 16u) == 0u) break; if ((folded[cursor] & 0xffffu) == spv::OpIAdd && folded[cursor + 4u] == expectedConstant) addedThirty = true; - if ((folded[cursor] & 0xffffu) == spv::OpExtInst && folded[cursor + 4u] == 1u) keptCustomExtInst = true; } check(addedThirty, "a constant-index private composite table load did not fold to its element constant"); - check(keptCustomExtInst, "a non-GLSL.std.450 OpExtInst was not preserved"); const auto [dynamic, unusedDynamic] = build(true, false); static_cast(unusedDynamic); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 433c58aad1..35d5e131dd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -446,7 +446,7 @@ std::uint32_t genericFormattedLoad(SpirvValueEmitContext& context, const IrValue const auto fromMemory = Binary(state, spv::OpUGreaterThanEqual, boolean, selectors[output], constant(4u)); required[component] = Binary(state, spv::OpLogicalOr, boolean, required[component], both(state, fromMemory, equal(state, selected[output], constant(component)))); } - required[component] = both(state, active[component], required[component]); + required[component] = Select(state, boolean, active[component], required[component], ConstantBool(state, false)); const auto componentOffset = field(state, byteOffsets, component * 8u, 8u); const auto bytes = Select(state, u32, packed, constant(4u), Binary(state, spv::OpShiftRightLogical, u32, bits[component], constant(3u))); const auto offset = Binary(state, spv::OpIAdd, u32, resource.offset != 0u ? resource.offset : Binary(state, spv::OpIAdd, u32, context.Arg(instruction, 2u), constant(memory.offset)), componentOffset); @@ -454,16 +454,16 @@ std::uint32_t genericFormattedLoad(SpirvValueEmitContext& context, const IrValue if (memory.gpuDescriptor) { const auto u64 = TypeScalarU64(state); const auto guest = Binary(state, spv::OpIAdd, u64, resource.base, Unary(state, spv::OpUConvert, u64, addresses[component].guest)); - const auto overflow = both(state, active[component], Binary(state, spv::OpULessThan, boolean, guest, resource.base)); + const auto overflow = Select(state, boolean, active[component], Binary(state, spv::OpULessThan, boolean, guest, resource.base), ConstantBool(state, false)); EmitIfCondition(state, overflow, [&] { RecordBdaFault(state, resource.base, constant(0u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Overflow); }); StopBdaInvocationIf(state, overflow); addresses[component].guest = guest; } - inBounds = both(state, inBounds, Binary(state, spv::OpLogicalOr, boolean, Unary(state, spv::OpLogicalNot, boolean, required[component]), addresses[component].inBounds)); + inBounds = Select(state, boolean, required[component], both(state, inBounds, addresses[component].inBounds), inBounds); } std::array values{}; for (std::uint32_t component = 0; component < 4u; ++component) { - values[component] = EmitValueOrZeroIfCondition(state, both(state, inBounds, required[component]), [&] { + values[component] = EmitValueOrZeroIfCondition(state, Select(state, boolean, required[component], inBounds, ConstantBool(state, false)), [&] { const auto width = Select(state, u32, packed, constant(32u), bits[component]); const auto dword = equal(state, width, constant(32u)); const auto raw = memory.gpuDescriptor diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index 2b7b1eb792..b20fa14351 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -100,7 +100,7 @@ private: std::vector present; }; -bool PureInstruction(const Instruction& instruction, const IdSet& glslExtInstSets = {}) { +bool PureInstruction(const Instruction& instruction) { switch (Opcode(instruction)) { case spv::OpVectorExtractDynamic: case spv::OpVectorInsertDynamic: @@ -219,8 +219,6 @@ bool PureInstruction(const Instruction& instruction, const IdSet& glslExtInstSet case spv::OpArrayLength: case spv::OpPhi: return true; - case spv::OpExtInst: - return instruction.size() >= 5u && glslExtInstSets.contains(instruction[3]); case spv::OpLoad: return instruction.size() == 4u || (instruction.size() == 5u && instruction[4] == spv::MemoryAccessMaskNone); default: @@ -233,9 +231,6 @@ bool VisitInputs(const Instruction& instruction, const TVisitor& visit) { std::size_t first = 3u; std::size_t end = instruction.size(); switch (Opcode(instruction)) { - case spv::OpExtInst: - first = 5u; - break; case spv::OpVectorShuffle: case spv::OpCompositeInsert: end = 5u; @@ -583,7 +578,6 @@ public: values.Reset(bound); resultTypes.Reset(bound); vectorTypes.Reset(bound); - glslExtInstSets.Reset(bound); removed.Reset(bound); aliases.Reset(bound); composites.Reset(bound); @@ -598,7 +592,6 @@ public: bool hasType = false; spv::HasResultAndType(op, &hasResult, &hasType); if (hasResult && hasType) resultTypes.emplace(instruction.at(2), instruction.at(1)); - if (op == spv::OpExtInstImport && instruction.size() == 6u && instruction[2] == 0x4c534c47u && instruction[3] == 0x6474732eu && instruction[4] == 0x3035342eu && instruction[5] == 0u) glslExtInstSets.insert(instruction.at(1)); if (op == spv::OpTypeBool) types.emplace(instruction.at(1), ScalarType{1u, true}); if (op == spv::OpTypeInt) types.emplace(instruction.at(1), ScalarType{instruction.at(2), false}); if (op == spv::OpTypeVector) vectorTypes.insert(instruction.at(1)); @@ -680,18 +673,6 @@ private: return pointer->second < elements.size() ? value(elements[pointer->second]) : std::nullopt; } const auto left = value(instruction[3]); - if (op == spv::OpLogicalAnd && instruction.size() == 5u) { - const auto right = value(instruction[4]); - if ((left && *left == 0u) || (right && *right == 0u)) return 0u; - if (left && right) return *left != 0u && *right != 0u; - return std::nullopt; - } - if (op == spv::OpLogicalOr && instruction.size() == 5u) { - const auto right = value(instruction[4]); - if ((left && *left != 0u) || (right && *right != 0u)) return 1u; - if (left && right) return *left != 0u || *right != 0u; - return std::nullopt; - } if (!left) return std::nullopt; if (op == spv::OpCopyObject) return left; if (op == spv::OpLogicalNot) return *left == 0u; @@ -1128,7 +1109,7 @@ private: } continue; } - if (PureInstruction(instruction, glslExtInstSets)) { + if (PureInstruction(instruction)) { definitions.emplace(Result(instruction), index); continue; } @@ -1165,7 +1146,6 @@ private: IdTable values; IdTable resultTypes; IdSet vectorTypes; - IdSet glslExtInstSets; IdSet removed; IdTable aliases; IdTable> composites; From c6ce329c474d05197b39b77e8dfcf09884ba13ae Mon Sep 17 00:00:00 2001 From: roboterspinne <229981445+roboterspinne@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:20:34 +0000 Subject: [PATCH 137/318] fix(relinker): reject link-time relocation types in executables The executable's relocation allowlist accepted R_X86_64_GOT32, PLT32, GOTPCREL, 32S, GOTPCRELX and REX_GOTPCRELX, which the linker resolves and glibc's ld.so does not apply. The default and --lazy-binding Linux output copied them from DT_RELA into its dynamic relocation table, and --linux-fixed-base did so for 32S, so the relink succeeded and ld.so then refused to load the program ("unexpected reloc type"). They are now rejected when the executable is validated, for every target. --- core/relinker/relinker/src/analysis/ValidationPolicy.cpp | 6 ------ .../relinker/tests/test_executable_tls_relocations.py | 9 +++++++-- core/relinker/relinker/tests/test_linux_fixed_base.py | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index dfd0a11798..47d65ac011 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -11,20 +11,14 @@ void ValidationPolicy::_initializeSupportedRelocationTypes() { R_X86_64_NONE, R_X86_64_64, R_X86_64_PC32, - R_X86_64_GOT32, - R_X86_64_PLT32, R_X86_64_COPY, R_X86_64_GLOB_DAT, R_X86_64_JUMP_SLOT, R_X86_64_RELATIVE, - R_X86_64_GOTPCREL, R_X86_64_32, - R_X86_64_32S, R_X86_64_DTPMOD64, R_X86_64_DTPOFF64, R_X86_64_TPOFF64, - R_X86_64_GOTPCRELX, - R_X86_64_REX_GOTPCRELX, }; } diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index fd6ceff424..b1857f1556 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -3,7 +3,9 @@ has them in its GOT. The Linux ELF keeps them dynamic and symbolless, with the v TLS block as the addend, for the dynamic loader to resolve against where it puts the block; a named one against a defined STT_TLS symbol is folded the same way, and one against an import stays named, typed STT_TLS. The Windows and macOS images resolve TPOFF64 and DTPOFF64 statically against their block, -AlignUp(p_memsz, p_align) below the thread pointer, and reject what they cannot express.""" +AlignUp(p_memsz, p_align) below the thread pointer, and reject what they cannot express. Every target +rejects the link-time types GOT32, PLT32, GOTPCREL, 32S, GOTPCRELX and REX_GOTPCRELX, which glibc's +ld.so does not apply.""" from pathlib import Path import struct @@ -18,6 +20,8 @@ from test_tls_function_coverage import pe_bytes_at PT_LOAD = 1 PT_TLS = 7 DTPMOD64, DTPOFF64, TPOFF64, TPOFF32 = 16, 17, 18, 23 +LINK_TIME = {3: "got32", 4: "plt32", 9: "gotpcrel", 11: "32s", 41: "gotpcrelx", 42: "rex-gotpcrelx"} +TARGETS = {(): "linux", ("--lazy-binding",): "lazy", ("--windows",): "windows", ("--macos",): "macos"} TLS_MEMSZ, TLS_ALIGN = 0x24, 16 TLS_BLOCK = 0x30 STRINGS = b"\0Tls\0Data\0ImportedTls\0" @@ -142,7 +146,8 @@ def main(): ("windows-import", [(0x1000, TPOFF64, IMPORTED, 0)], True, ("--windows",), "Imported TLS relocation"), ("macos-dtpmod", [(0x1000, DTPMOD64, NULL, 0)], True, ("--macos",), "DTPMOD64 relocation is not supported"), ("macos-import", [(0x1000, TPOFF64, IMPORTED, 0)], True, ("--macos",), "Imported TLS relocation"), - ] + ] + [(f"{target}-{name}", [(0x1000, kind, NULL, 0)], True, flags, f"Unsupported relocation type {kind:#x}") + for kind, name in LINK_TIME.items() for flags, target in TARGETS.items()] for name, relocations, has_tls, flags, error in failures: result, output = relink(relinker, directory, name, image(relocations, has_tls), *flags) assert result.returncode == 2 and error in result.stderr, (name, result.returncode, result.stderr) diff --git a/core/relinker/relinker/tests/test_linux_fixed_base.py b/core/relinker/relinker/tests/test_linux_fixed_base.py index 84244f4533..6b9d45466b 100644 --- a/core/relinker/relinker/tests/test_linux_fixed_base.py +++ b/core/relinker/relinker/tests/test_linux_fixed_base.py @@ -154,7 +154,7 @@ def main(): for name, image, base, error in ( ("unaligned", fixture(alignment=0x20000), 0x410000, "is not a multiple of the PT_LOAD alignment 0x20000"), ("beyond-user-space", fixture(memory_size=0x100000), 0x7FFFFFFF0000, "outside the Linux user address space"), - ("unsupported-type", fixture(relocation_type=9), BASE, "Relocation type 9 cannot be placed at a fixed Linux base"), + ("unsupported-type", fixture(relocation_type=11), BASE, "Unsupported relocation type 0xb"), ("negative-target", fixture(addend=-16), BASE, "Relocation target -16 lies below the image at a fixed Linux base"), ): result, output = relink(relinker, work, name, image, "--linux-fixed-base", hex(base)) From 959efbb8d319d0a0ee22f11cf38a8301c73e14c2 Mon Sep 17 00:00:00 2001 From: zmeizy Date: Wed, 21 Oct 2026 18:29:55 +0300 Subject: [PATCH 138/318] fix(shader): take the hardware f32 flush only in shaders that flush f32 denormals When the startup probe chose the hardware f32 flush, every module declared DenormFlushToZero 32, also shaders whose FLOAT_MODE keeps f32 denormals (f32 denormal field 1, 2 or 3), so the device flushed what they keep and DenormPreserve 32 was never declared. The hardware flush now applies to shaders that flush f32 denormals (field 0, or no float mode); the others declare DenormPreserve 32 where the device supports it. Co-Authored-By: Claude Opus 5.5 --- .../tests/F32DenormPreserve.cpp | 19 ++++++++++++++++--- core/shader/recompiler/Recompiler.cpp | 8 ++++++-- docs/dev/debt/shader.md | 1 + 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp index 0f3fe85da7..f47e2d0ebc 100644 --- a/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/F32DenormPreserve.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -19,17 +20,19 @@ void require(bool condition, const std::string& message) { } constexpr std::array CvtF16F32Store{0x7e021500u, 0xe0700000u, 0x80000100u, 0xbf810000u}; +constexpr std::array MovStore{0x7e020300u, 0xe0700000u, 0x80000100u, 0xbf810000u}; -std::vector recompile(bool preserve, bool flush, bool flushCapability = true) { +std::vector recompile(bool preserve, bool flush, bool flushCapability = true, std::optional floatMode = std::nullopt, std::span code = CvtF16F32Store) { const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityDenormPreserve, spv::CapabilityDenormFlushToZero}; const std::array userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu}; const std::array extensions{"SPV_KHR_storage_buffer_storage_class", "SPV_KHR_float_controls"}; RecompileRequest request{}; - request.shader = {ShaderStage::Compute, 0x20000u, CvtF16F32Store, 0, {}}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; request.context.waveSize = 64; request.context.userDataBaseRegister = 0; request.context.userData = userData; request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.context.floatMode = floatMode; request.target.vulkanVersion = 0x00403000u; request.target.spirvVersion = 0x00010600u; request.target.subgroupSize = 64; @@ -79,6 +82,16 @@ void verify() { require(flushed.flush32 && !flushed.preserve32, "the hardware 32-bit flush did not take precedence over the preserve flag"); const auto nativeFlush = scan(recompile(true, true, false)); require(!nativeFlush.preserve32 && !nativeFlush.flush32, "a hardware flush without the DenormFlushToZero capability declared a 32-bit denormal execution mode"); + const auto flushingMode = scan(recompile(true, true, true, ShaderFloatMode{0xc0u, true, false, false}, MovStore)); + require(flushingMode.flush32 && !flushingMode.preserve32, "FLOAT_MODE 0xc0 flushes f32 denormals but the module did not take the hardware flush"); + for (const std::uint32_t mode : {0xd0u, 0xe0u, 0xf0u}) { + char name[64]; + std::snprintf(name, sizeof(name), "FLOAT_MODE 0x%02x keeps f32 denormals", mode); + const auto kept = scan(recompile(true, true, true, ShaderFloatMode{mode, true, false, false}, MovStore)); + require(kept.preserve32 && kept.preserveCapability && !kept.flush32, std::string(name) + ", but the module did not declare DenormPreserve 32 instead of the hardware flush"); + const auto unpreserved = scan(recompile(false, true, true, ShaderFloatMode{mode, true, false, false}, MovStore)); + require(!unpreserved.flush32 && !unpreserved.preserve32, std::string(name) + ", but the module took the hardware flush on a device that cannot preserve them"); + } } } @@ -90,6 +103,6 @@ int main() { std::fprintf(stderr, "F32DenormPreserve: %s\n", error.what()); return 1; } - std::puts("F32DenormPreserve: the 32-bit DenormPreserve execution mode follows the target flag and yields to the hardware flush"); + std::puts("F32DenormPreserve: the 32-bit DenormPreserve execution mode follows the target flag and yields to the hardware flush in shaders that flush f32 denormals"); return 0; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ad94823d64..bae14c10ed 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -153,6 +153,10 @@ std::uint32_t DeviceMemoryLdsBytes(const RecompileRequest& request) { return bytes + 4u > request.target.maxWorkgroupSharedMemoryBytes ? static_cast(bytes) : 0u; } +bool HardwareFloat32Flush(const RecompileRequest& request) { + return request.target.float32FlushToZero && (!request.context.floatMode.has_value() || ((request.context.floatMode->floatMode >> 4u) & 3u) == 0u); +} + std::atomic frontEndBuilds{0}; } @@ -239,7 +243,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request, ShaderPreparat translateOptions.nativeFloat16 = supports(spv::CapabilityFloat16) && supports(spv::CapabilityRoundingModeRTE) && supports(spv::CapabilityDenormPreserve); translateOptions.interpolationFunctions = supports(spv::CapabilityInterpolationFunction); translateOptions.floatMode = request.context.floatMode; - translateOptions.float32FlushToZero = request.target.float32FlushToZero; + translateOptions.float32FlushToZero = HardwareFloat32Flush(request); translateOptions.inputInfo = inputInfo; constexpr InstructionTranslator translator; @@ -602,7 +606,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.float16DenormPreserve = request.target.float16DenormPreserve; targetOptions.spannedWaves = request.target.spannedWaves; targetOptions.robustBufferAccess2 = request.target.robustBufferAccess2; - targetOptions.float32FlushToZero = request.target.float32FlushToZero; + targetOptions.float32FlushToZero = HardwareFloat32Flush(request); targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d76932d773..9a4ac1f273 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -116,6 +116,7 @@ - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` flush both operands by the field, see the LDS float atomics entry. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. +- [32-bit denormal execution mode](../../../core/shader/recompiler/Recompiler.cpp) (shader recompiler) - the hardware f32 flush that the startup probe chooses (`DenormFlushToZero 32`) is declared only in shaders whose `FLOAT_MODE` flushes f32 denormals (field 0, or no known float mode). A shader that keeps them on either side (field 1, 2 or 3) declares `DenormPreserve 32` when the device has `shaderDenormPreserveFloat32` with a `denormBehaviorIndependence` other than NONE; on other devices it gets the driver's default f32 denormal handling, which flushes the denormals it keeps on MoltenVK. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler - [16-bit integer multiplies](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`v_mul_lo_u16`, `v_pk_mul_lo_u16`) (shader recompiler) - a factor taken from the low half of its source is read as the low 24 bits of the register, not the low 16; the low 16 bits of the product are the same for every input. The AMD Windows driver 32.0.21045.5002 (RX 6700 XT, RDNA 2) stores the full 32-bit product when two values it knows fit in 16 bits are multiplied, the product is reduced to 16 bits and nothing else is merged into the stored value: `v_mul_lo_u16` into a register whose high half is zero, also with factors masked to 16 bits by the program, and `v_pk_mul_lo_u16` whose high lanes are zero wrote 0xfffe0001 for 0xffff * 0xffff where the GPU writes 0x00000001. Reducing the product with `v_and_b32`, `v_bfe_u32` or a shift pair loses the same bits; factors read as 24-bit fields do not. The [constant folder](../../../core/shader/recompiler/Optimization/src/ConstantFolder.cpp) keeps a field read that a multiply uses, even when the program already masked the factor to 16 bits, since folding it away brings the full product back (#8271). Measured against the same instructions run through the [hardware oracle](../HW_ORACLE.md) on that GPU; other drivers are not checked From e3110be3a37343eecc9f486b645235d07b984f98 Mon Sep 17 00:00:00 2001 From: Amr Yasser <154547867+AMRYB@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:36:39 +0300 Subject: [PATCH 139/318] fix(tools): normalize Windows module suffix case before matching --- tools/import_audit.py | 4 +++- tools/tests/test_import_audit.py | 40 ++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 6ee58ad673..9e2c15b9fd 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -538,11 +538,13 @@ MODULE_FILE_SUFFIXES = (".debug_prx", ".suprx", ".sprx", ".prx") def module_stem(name, fold): + if fold: + name = name.lower() for suffix in MODULE_FILE_SUFFIXES: if len(name) > len(suffix) and name.endswith(suffix): name = name[:-len(suffix)] break - return name.lower() if fold else name + return name class ModuleNames(set): diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index a29775c13b..ff0787e7ed 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -605,6 +605,46 @@ class ImportAuditTests(unittest.TestCase): records, missing = import_audit.audit(imports, {}, {}, set(), import_audit.ModuleNames({"sdk-shipping.prx"})) self.assertEqual((records[0]["class"], missing), ("absent", {"SDK-Shipping.debug_prx": 1})) + def test_windows_module_suffix_case_does_not_report_existing_modules_as_missing(self): + work = self.work + (work.libs / "libc.prx").write_bytes(build_pe([])) + registry, report = work.root / "game.registry.json", work.root / "audit.json" + cases = (("sdk-shipping.prx", "SDK-Shipping.DEBUG_PRX"), + ("SDK-SHIPPING.PRX", "sdk-shipping.sprx"), + ("Sdk-Shipping.SuPrX", "SDK-SHIPPING.debug_prx"), + ("SDK-SHIPPING.DEBUG_PRX", "sdk-shipping.PRX")) + for filename, needed in cases: + with self.subTest(filename=filename, needed=needed): + module = work.modules / filename + module.write_bytes(exporting("AAAAAAAAAAA")) + try: + write_registry(registry, [("AAAAAAAAAAA", needed)]) + import_audit.needed_registry_path(registry).write_text(json.dumps([needed]), encoding="utf-8") + code, _, err = work.run(registry, "--json", str(report), "--strict-providers") + self.assertEqual((code, err), (0, "")) + data = json.loads(report.read_text(encoding="utf-8")) + self.assertEqual(data["missing_libraries"], {}) + self.assertEqual(data["unique_by_class"], {"implemented": 0, "stub": 0, "absent": 0, "module": 1}) + self.assertEqual(data["references"], 1) + self.assertEqual(data["imports"][0]["library"], needed) + finally: + module.unlink() + + def test_elf_module_suffix_case_stays_significant(self): + work = self.work + (work.libs / "libc.prx").write_bytes(exporting()) + (work.modules / "sdk-shipping.prx").write_bytes(exporting("AAAAAAAAAAA")) + registry, report = work.root / "game.registry.json", work.root / "audit.json" + for suffix in ("PRX", "SPRX", "SUPRX", "DEBUG_PRX"): + needed = "sdk-shipping." + suffix + with self.subTest(needed=needed): + write_registry(registry, [("AAAAAAAAAAA", needed)]) + code, _, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + data = json.loads(report.read_text(encoding="utf-8")) + self.assertEqual(data["missing_libraries"], {needed: 1}) + self.assertEqual(data["unique_by_class"], {"implemented": 0, "stub": 0, "absent": 1, "module": 0}) + def test_converted_module_matches_imports_of_its_base_name(self): work = self.work (work.libs / "libc.prx").write_bytes(exporting()) From c6f86e1cc333c3b57daac86e8dfffb8474dfc32e Mon Sep 17 00:00:00 2001 From: slopggobblercobbler81 Date: Wed, 21 Oct 2026 17:42:24 +0200 Subject: [PATCH 140/318] fix(relinker): validate relocation types during table extraction Validate relocation types using validationPolicy immediately upon reading rInfo in extractRela, ensuring unsupported relocation types are rejected at their table offset before symbol table index lookups or TLS checks occur. Reject non-JUMP_SLOT relocation types in DT_JMPREL to prevent invalid PLT dispatch and downstream filter/builder errors. --- .../src/pipeline/RelinkerPipeline.cpp | 8 ++-- .../relinker/tests/ConversionTests.cpp | 44 +++++++++++++++++++ .../tests/test_executable_tls_relocations.py | 2 + .../tests/test_jmprel_table_bounds.py | 5 +++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 78ce7cd35a..77044446d2 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -346,9 +346,14 @@ RelinkResult RelinkerPipeline::Relink() { const std::uint32_t symIdx = static_cast(rInfo >> 32); const std::uint32_t relType = static_cast(rInfo & 0xffffffff); + validationPolicy->ValidateRelocationTypeSupported(relType, pos); + if (relType == R_X86_64_NONE) continue; + if (plt && relType != R_X86_64_JUMP_SLOT) + throw RelinkerException("Unsupported PLT relocation type", pos); + if (relType == 8) { if (symIdx != 0 && !isZeroSectionSymbol(symIdx)) throw RelinkerException("RELATIVE relocation has a nonzero symbol index", pos); @@ -377,9 +382,6 @@ RelinkResult RelinkerPipeline::Relink() { extractRela(dynRelaOffset, dynRelaSize, false); extractRela(dynJmpRelOffset, dynJmpRelSize, true); - for (const auto& ref : nidRefs) - validationPolicy->ValidateRelocationTypeSupported(ref.RelocationTypeValue, ref.RelocationTableOffset); - std::vector> unwindRanges; std::vector> segmentUnwindRanges(executableSegments.size()); if (std::any_of(programHeaders.begin(), programHeaders.end(), [](const ProgramHeader& ph) { return ph.Type == PT_SCE_DYNLIBDATA; })) { diff --git a/core/relinker/relinker/tests/ConversionTests.cpp b/core/relinker/relinker/tests/ConversionTests.cpp index 468921cf4f..07c35d6e9a 100644 --- a/core/relinker/relinker/tests/ConversionTests.cpp +++ b/core/relinker/relinker/tests/ConversionTests.cpp @@ -292,6 +292,49 @@ void runtimeDirectories() { } } +void relocationValidation() { + TestProgress progress; + const Relinker::Conversion converter(progress); + { + auto source = input(); + Io::WriteU64(source.Executable, 0x2058, 24); + Io::WriteU64(source.Executable, 0x2700, 0x2000); + Io::WriteU64(source.Executable, 0x2708, (static_cast(0x9999) << 32) | 0x99); + Io::WriteU64(source.Executable, 0x2710, 0); + try { + converter.Convert(source, {}); + throw std::runtime_error("Unsupported relocation type was accepted"); + } catch (const Domain::RelinkerException& error) { + require(std::string(error.what()).find("Unsupported relocation type 0x99") != std::string::npos, "Wrong error for unsupported relocation"); + require(error.FailureOffset == 0x2700, "Wrong failure offset for unsupported relocation"); + } + } + { + auto source = input(); + const std::vector> tags{ + {5, 0x2600}, {10, 1}, {6, 0x2620}, {11, 24}, {7, 0x2700}, {8, 0}, {9, 24}, {4, 0x2680}, + {3, 0x2000}, {20, 7}, {23, 0x2780}, {2, 24}, {0, 0} + }; + const auto at = 64 + 2 * 56; + Io::WriteU64(source.Executable, at + 32, tags.size() * 16); + Io::WriteU64(source.Executable, at + 40, tags.size() * 16); + for (std::size_t index = 0; index < tags.size(); ++index) { + Io::WriteU64(source.Executable, 0x2000 + index * 16, tags[index].first); + Io::WriteU64(source.Executable, 0x2008 + index * 16, tags[index].second); + } + Io::WriteU64(source.Executable, 0x2780, 0x2000); + Io::WriteU64(source.Executable, 0x2788, (static_cast(1) << 32) | 8); + Io::WriteU64(source.Executable, 0x2790, 0); + try { + converter.Convert(source, {}); + throw std::runtime_error("Unsupported PLT relocation type was accepted"); + } catch (const Domain::RelinkerException& error) { + require(std::string(error.what()) == "Unsupported PLT relocation type", "Wrong error for unsupported PLT relocation"); + require(error.FailureOffset == 0x2780, "Wrong failure offset for unsupported PLT relocation"); + } + } +} + } int main() { @@ -303,6 +346,7 @@ int main() { cancellation(); baseRelocations(); runtimeDirectories(); + relocationValidation(); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; diff --git a/core/relinker/relinker/tests/test_executable_tls_relocations.py b/core/relinker/relinker/tests/test_executable_tls_relocations.py index fd6ceff424..b18e6f1d32 100644 --- a/core/relinker/relinker/tests/test_executable_tls_relocations.py +++ b/core/relinker/relinker/tests/test_executable_tls_relocations.py @@ -135,6 +135,8 @@ def main(): failures = [ ("tpoff32", [(0x1000, TPOFF32, NULL, 0)], True, (), "Unsupported relocation type 0x17"), + ("tpoff32-invalid-sym", [(0x1000, TPOFF32, 0x1234, 0)], True, (), "Unsupported relocation type 0x17"), + ("unknown-reloc-invalid-sym", [(0x1000, 0x99, 0x1234, 0)], True, (), "Unsupported relocation type 0x99"), ("not-tls-symbol", [(0x1000, TPOFF64, DATA_SYMBOL, 0)], True, (), "not STT_TLS"), ("no-tls-segment", [(0x1000, TPOFF64, NULL, 0)], False, (), "without a PT_TLS segment"), ("dtpmod-addend", [(0x1000, DTPMOD64, NULL, 1)], True, (), "DTPMOD64 relocation has a nonzero addend"), diff --git a/core/relinker/relinker/tests/test_jmprel_table_bounds.py b/core/relinker/relinker/tests/test_jmprel_table_bounds.py index 504e1f9109..ed7bb8f41e 100644 --- a/core/relinker/relinker/tests/test_jmprel_table_bounds.py +++ b/core/relinker/relinker/tests/test_jmprel_table_bounds.py @@ -124,6 +124,11 @@ def main(): "Symbol table entry out of bounds", target=output_target) run(relinker, work, prefix + "-none", fixture(symbol_bytes=0, symbol_index=0xFFFFFFFF, relocation_type=0, **options), target=output_target) + else: + run(relinker, work, prefix + "-unsupported-plt-type", fixture(relocation_type=6, **options), + "Unsupported PLT relocation type", target=output_target) + run(relinker, work, prefix + "-relative-in-plt", fixture(relocation_type=8, **options), + "Unsupported PLT relocation type", target=output_target) print("Jump relocation table bounds tests passed") From 059d3f2a1620f50a1e2a090cdd1688eda78ca6d8 Mon Sep 17 00:00:00 2001 From: zmeizy Date: Wed, 14 Oct 2026 21:03:03 +0300 Subject: [PATCH 141/318] fix(shader): round v_cvt_pk_u8_f32 in the FLOAT_MODE f32 round mode v_cvt_pk_u8_f32 rounds its source in the f32 round mode of FLOAT_MODE and flushes a denormal source when f32 denormals are flushed, as measured on RDNA2 (gfx1030). It was always rounded to nearest even, so the three directed modes gave wrong bytes for non-integer sources. Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/VCvtPkU8F32RoundMode.cpp | 141 ++++++++++++++++++ .../Translation/src/ConvertInstructions.cpp | 13 +- docs/dev/debt/shader.md | 1 + 3 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp new file mode 100644 index 0000000000..777bd3e290 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/VCvtPkU8F32RoundMode.cpp @@ -0,0 +1,141 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Inputs = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084u, 0x34040082u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0xd55e000au, 0x02010104u, 0xe0701000u, + 0x80010a02u, 0xbf810000u, +}; + +struct PackVector { + std::uint32_t value; + std::uint32_t denormalsFlushed[4]; + std::uint32_t denormalsKept[4]; +}; + +constexpr std::array Vectors{{ + {0x00000000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x3e000000u, {0x00u, 0x01u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x3e800000u, {0x00u, 0x01u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x3f000000u, {0x00u, 0x01u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x3f400000u, {0x01u, 0x01u, 0x00u, 0x00u}, {0x01u, 0x01u, 0x00u, 0x00u}}, + {0x3f800000u, {0x01u, 0x01u, 0x01u, 0x01u}, {0x01u, 0x01u, 0x01u, 0x01u}}, + {0x3fa00000u, {0x01u, 0x02u, 0x01u, 0x01u}, {0x01u, 0x02u, 0x01u, 0x01u}}, + {0x3fc00000u, {0x02u, 0x02u, 0x01u, 0x01u}, {0x02u, 0x02u, 0x01u, 0x01u}}, + {0x40200000u, {0x02u, 0x03u, 0x02u, 0x02u}, {0x02u, 0x03u, 0x02u, 0x02u}}, + {0x40600000u, {0x04u, 0x04u, 0x03u, 0x03u}, {0x04u, 0x04u, 0x03u, 0x03u}}, + {0x40700000u, {0x04u, 0x04u, 0x03u, 0x03u}, {0x04u, 0x04u, 0x03u, 0x03u}}, + {0x42ff0000u, {0x80u, 0x80u, 0x7fu, 0x7fu}, {0x80u, 0x80u, 0x7fu, 0x7fu}}, + {0x43008000u, {0x80u, 0x81u, 0x80u, 0x80u}, {0x80u, 0x81u, 0x80u, 0x80u}}, + {0x43482000u, {0xc8u, 0xc9u, 0xc8u, 0xc8u}, {0xc8u, 0xc9u, 0xc8u, 0xc8u}}, + {0x437e8000u, {0xfeu, 0xffu, 0xfeu, 0xfeu}, {0xfeu, 0xffu, 0xfeu, 0xfeu}}, + {0x437ea000u, {0xffu, 0xffu, 0xfeu, 0xfeu}, {0xffu, 0xffu, 0xfeu, 0xfeu}}, + {0x437ee000u, {0xffu, 0xffu, 0xfeu, 0xfeu}, {0xffu, 0xffu, 0xfeu, 0xfeu}}, + {0x437f0000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x437f4000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x437f7fffu, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x437f8000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x437fe000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x43800000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0x501502f9u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0xbe000000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0xbf000000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0xbeffffffu, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0xbf800000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0xd01502f9u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x00000001u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x80000001u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x007fffffu, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x00800000u, {0x00u, 0x01u, 0x00u, 0x00u}, {0x00u, 0x01u, 0x00u, 0x00u}}, + {0x7f800000u, {0xffu, 0xffu, 0xffu, 0xffu}, {0xffu, 0xffu, 0xffu, 0xffu}}, + {0xff800000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x7fc00000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0xffc00000u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, + {0x7f800001u, {0x00u, 0x00u, 0x00u, 0x00u}, {0x00u, 0x00u, 0x00u, 0x00u}}, +}}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((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; +} + +void Run(AgcDriver::VulkanDevice& device, ShaderRecompiler::ShaderFloatMode floatMode) { + Input.fill(0u); + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) Input[lane * Inputs] = Vectors[lane].value; + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device.Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = floatMode; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(bool kept, std::uint32_t rounding, bool ieee) { + for (std::uint32_t lane = 0; lane < Vectors.size(); ++lane) { + const auto& vector = Vectors[lane]; + const auto expected = kept ? vector.denormalsKept[rounding] : vector.denormalsFlushed[rounding]; + Require(Output[lane] == expected, std::string("v_cvt_pk_u8_f32 round mode ") + std::to_string(rounding) + (kept ? ", f32 denormals kept" : ", f32 denormals flushed") + + (ieee ? ", IEEE=1" : ", IEEE=0") + " of " + Hex(vector.value) + " is " + Hex(Output[lane]) + ", expected " + Hex(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const bool kept : {false, true}) { + for (std::uint32_t rounding = 0; rounding < 4u; ++rounding) { + for (const bool ieee : {false, true}) { + Run(*device, ShaderRecompiler::ShaderFloatMode{(kept ? 0xf0u : 0xc0u) | rounding, true, ieee, false}); + Check(kept, rounding, ieee); + } + } + } + std::puts("v_cvt_pk_u8_f32 round mode tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp index d14b698c77..4dfdf3ed3b 100644 --- a/core/shader/recompiler/Translation/src/ConvertInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ConvertInstructions.cpp @@ -215,15 +215,22 @@ void TranslationContext::vCvtPknormF32(const RdnaInstruction& inst, bool signedV } void TranslationContext::vCvtPkU8F32(const RdnaInstruction& inst) { - const IrF32 source(ir.BitCastF32(*readOperand(sourceAt(inst, 0u), IrType::U32))); + const IrU32 bits = flushF32Denormal(IrU32(*readOperand(sourceAt(inst, 0u), IrType::U32))); + const IrF32 source(ir.BitCastF32(bits.Value())); const IrF32 zero(ir.ConstantF32(0.0f)); const IrU1 nan(ir.Emit(IrOpcode::FPIsNan32, IrType::U1, {&source.Value()})); const IrU1 low(ir.Emit(IrOpcode::FPOrdLessThanEqual32, IrType::U1, {&source.Value(), &zero.Value()})); const IrU1 high(ir.Emit(IrOpcode::FPOrdGreaterThanEqual32, IrType::U1, {&source.Value(), &ir.ConstantF32(255.0f)})); - const IrF32 rounded(ir.Emit(IrOpcode::FPRoundEven32, IrType::F32, {&source.Value()})); + const std::uint32_t roundMode = floatMode.has_value() ? floatMode->floatMode & 3u : 0u; + const IrOpcode rounding = roundMode == 1u ? IrOpcode::FPCeil32 : roundMode == 2u ? IrOpcode::FPFloor32 : roundMode == 3u ? IrOpcode::FPTrunc32 : IrOpcode::FPRoundEven32; + const IrF32 rounded(ir.Emit(rounding, IrType::F32, {&source.Value()})); const IrF32 bounded = selectF32(high, IrF32(ir.ConstantF32(255.0f)), rounded); const IrF32 safe = selectF32(IrU1(ir.LogicalOr(nan.Value(), low.Value())), zero, bounded); - const IrU32 byteValue(ir.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&safe.Value()})); + IrU32 byteValue(ir.Emit(IrOpcode::ConvertU32F32, IrType::U32, {&safe.Value()})); + if (roundMode == 1u && (f32DenormalFlush & 1u) == 0u) { + const IrU1 positiveDenormal(ir.ULessThan(ir.ISub(bits.Value(), ir.Constant(1u)), ir.Constant(0x007fffffu))); + byteValue = IrU32(ir.Select(positiveDenormal.Value(), ir.Constant(1u), byteValue.Value())); + } const IrU32 index(ir.BitwiseAnd(readU32(sourceAt(inst, 1u)).Value(), ir.Constant(3u))); const IrU32 shift(ir.ShiftLeftLogical(index.Value(), ir.Constant(3u))); const IrU32 mask(ir.ShiftLeftLogical(ir.Constant(0xffu), shift.Value())); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index d76932d773..5735d758d3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -76,6 +76,7 @@ - [Bindless image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. - [Shader MODE register access](../../../core/shader/recompiler/Translation/src/RoundingMode.cpp) starts from the rounding fields of `GuestContext::floatMode` and follows immediate rounding writes (`s_round_mode`, `s_setreg_imm32_b32` on MODE bits 0-3) through the control flow graph; `s_getreg_b32` of those bits returns the tracked value. Only `v_cvt_f32_i32`, `v_cvt_f32_u32`, `v_ldexp_f32`, the f16 conversions and `v_ldexp_f16` (with f16 denormals kept) and the instructions that already read `FLOAT_MODE` follow a rounding mode set at run time; any other float instruction that runs while a directed rounding mode differs from the initial one throws at that instruction. Rounding bits written from an SGPR (`s_setreg_b32`) or that differ between the paths reaching an instruction throw where they are read or used. A shader without `FLOAT_MODE` metadata throws on any rounding change. Non-rounding register reads still throw; `s_setreg_b32` writes to other hardware registers are still ignored. Tests follow the public RDNA2/LLVM contract, not console measurements. - [Flat scalar-read reachability](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) keeps both successors active for non-clean predicates and loop headers, so an inaccessible direct root on such a path still refuses capture. Skipping a read under a clean, evaluable scalar branch follows RDNA2 conditional PC semantics and synthetic fixtures; console execution and title behavior are unverified. +- [v_cvt_pk_u8_f32](../../../core/shader/recompiler/Translation/src/ConvertInstructions.cpp) throws in f32 denormal modes 1 and 2 and with a directed round mode set at run time (see the previous entry); the PS5's gfx1013 is not measured. Otherwise it rounds its source in the `FLOAT_MODE` f32 round mode (ties to even, toward +infinity, -infinity or zero), clamps it to 0..255 (a NaN gives 0) and flushes a denormal source when f32 denormals are flushed. A kept positive denormal gives 1 toward +infinity, decided on the bits, as a driver without `DenormPreserve 32` may flush it. Measured on RDNA2 (gfx1030, RX 6800, AMD Windows driver 32.0.21045.5002) over 2,133 sources (k/8 for k = -16 to 2,099, values next to 0.5 and 255.5, ±1e10, denormals, infinities, NaNs) at IEEE 0 and 1, f32 denormal modes 0 and 3 and f32 round modes 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even, FP16_OVFL=0. - A [flat SRT read](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) through a null or unmapped pointer loaded from memory raises a [BDA fault](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp) only if the program executes it, then stops or reads zero like a BDA read. - Repeated `s_load`s of one [flat SRT](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) dword through a null or unmapped pointer report one of their pcs, and an `s_load` whose inaccessible dwords are all unused, or only address repeated loads, reports no fault of its own. - A value read through a null or unmapped pointer still throws at record time if a [uniform fill or table base](../../../core/shader/recompiler/Optimization/src/SrtWalker/SrtDescriptorEvaluation.cpp) needs it, or if the pointer comes from user data and is not null, even if the console skips the read. From 121dc64d11523c4b137975fb56717d7f248dca22 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 18:06:06 +0100 Subject: [PATCH 142/318] feat(libkernel_sys): implement pselect over select --- .../prx/libkernel_sys/Export.Discovery.cpp | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index 8b3da583e4..dcf727762f 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -353,9 +353,31 @@ int APS5_VABI profil_nid_postfix(void) { return 0; } -int APS5_VABI pselect_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); +int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); + +int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask) { + constexpr int SetMask = 3; + struct Timeval { + std::int64_t seconds; + std::int64_t microseconds; + } limit{}; + const Timeval* wait = nullptr; + if (timeout != nullptr) { + const auto* spec = static_cast(timeout); + limit = {spec[0], spec[1] / 1000}; + wait = &limit; + } + std::uint32_t previous[4]{}; + if (sigmask != nullptr && sigprocmask_nid_postfix(SetMask, sigmask, previous) != 0) return -1; + const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + if (sigmask != nullptr) { + const int error = *__error_nid_postfix(); + sigprocmask_nid_postfix(SetMask, previous, nullptr); + *__error_nid_postfix() = error; + } + return result; } int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void) { From 360206e6db68465f901c0821581948270789f8a7 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 18:06:06 +0100 Subject: [PATCH 143/318] test(libkernel_sys): cover pselect timeouts, readiness, errors and the signal mask --- core/libs/CMakeLists.txt | 10 ++ core/libs/tests/GuestKernelSysPselect.cpp | 108 ++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 core/libs/tests/GuestKernelSysPselect.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 737f8fa20f..5b327884d7 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -337,6 +337,16 @@ if(BUILD_TESTING) target_compile_options(crash_text_tests PRIVATE -fasynchronous-unwind-tables) endif() add_test(NAME crash_text COMMAND crash_text_tests) + add_test_executable(guest_kernel_sys_pselect_tests tests/GuestKernelSysPselect.cpp) + target_include_directories(guest_kernel_sys_pselect_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_kernel_sys_pselect_tests PRIVATE libkernel_sys libkernel libc) + configure_windows_unwind(guest_kernel_sys_pselect_tests) + add_test(NAME guest_kernel_sys_pselect COMMAND guest_kernel_sys_pselect_tests) + set_tests_properties(guest_kernel_sys_pselect PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_sys_pselect PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) add_test_executable(windows_crash_text_tests tests/WindowsCrashText.cpp) diff --git a/core/libs/tests/GuestKernelSysPselect.cpp b/core/libs/tests/GuestKernelSysPselect.cpp new file mode 100644 index 0000000000..e3924a7f43 --- /dev/null +++ b/core/libs/tests/GuestKernelSysPselect.cpp @@ -0,0 +1,108 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI pipe_nid_postfix(int* descriptors); +int APS5_VABI close_nid_postfix(int descriptor); +std::int64_t APS5_VABI write_nid_postfix(int descriptor, const char* buffer, std::int64_t size); +int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); +int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask); +} + +namespace { + +constexpr int Einval = 22; +constexpr int SigBlock = 1; +constexpr int SigSetmask = 3; +constexpr int SigUsr1 = 30; + +struct Timespec { + std::int64_t seconds; + std::int64_t nanoseconds; +}; + +struct SignalSet { + std::uint32_t bits[4]{}; +}; + +struct DescriptorSet { + std::array words{}; + void Add(int descriptor) { words[descriptor / 64] |= std::uint64_t{1} << (descriptor % 64); } + bool Has(int descriptor) const { return ((words[descriptor / 64] >> (descriptor % 64)) & 1u) != 0; } +}; + +void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "check failed at line %u\n", static_cast(location.line())); + std::exit(1); +} + +int Error() { + return *__error_nid_postfix(); +} + +SignalSet CurrentMask() { + SignalSet current{}; + Require(sigprocmask_nid_postfix(SigBlock, nullptr, ¤t) == 0); + return current; +} + +bool Same(const SignalSet& left, const SignalSet& right) { + return std::memcmp(left.bits, right.bits, sizeof(left.bits)) == 0; +} + +} + +int main() { + int descriptors[2]{-1, -1}; + Require(pipe_nid_postfix(descriptors) == 0); + const int reader = descriptors[0]; + const int writer = descriptors[1]; + SignalSet blocked{}; + blocked.bits[(SigUsr1 - 1) / 32] |= 1u << ((SigUsr1 - 1) % 32); + const SignalSet before = CurrentMask(); + { + DescriptorSet read{}; + read.Add(reader); + const Timespec limit{0, 200000000}; + const auto start = std::chrono::steady_clock::now(); + Require(pselect_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit, &blocked) == 0); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(150)); + Require(!read.Has(reader)); + Require(Same(CurrentMask(), before)); + } + Require(write_nid_postfix(writer, "x", 1) == 1); + { + DescriptorSet read{}; + read.Add(reader); + const Timespec limit{0, 999999999}; + Require(pselect_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, &limit, nullptr) == 1); + Require(read.Has(reader)); + } + { + DescriptorSet read{}; + read.Add(reader); + Require(pselect_nid_postfix(reader + 1, read.words.data(), nullptr, nullptr, nullptr, &blocked) == 1); + Require(read.Has(reader)); + Require(Same(CurrentMask(), before)); + } + { + const Timespec invalid{0, 1000000000}; + Require(pselect_nid_postfix(0, nullptr, nullptr, nullptr, &invalid, &blocked) == -1 && Error() == Einval); + Require(Same(CurrentMask(), before)); + const Timespec zero{0, 0}; + Require(pselect_nid_postfix(-1, nullptr, nullptr, nullptr, &zero, nullptr) == -1 && Error() == Einval); + } + Require(sigprocmask_nid_postfix(SigSetmask, &before, nullptr) == 0); + Require(close_nid_postfix(reader) == 0 && close_nid_postfix(writer) == 0); + std::puts("pselect passed"); + return 0; +} From 17650eed5f911ead4c7c32edf3a3d4815d56bca9 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 18:06:07 +0100 Subject: [PATCH 144/318] docs(debt): record that libkernel_sys pselect swaps the mask apart from the wait --- docs/dev/debt/libkernel_sys.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 25499ae3d1..ff16c7e9af 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -14,6 +14,7 @@ - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified +- [pselect](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) sets the signal mask with `sigprocmask`, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does, but the swap is not atomic with the wait: a signal that becomes deliverable between the swap and the wait is not guaranteed to interrupt it - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) From 01e7d158b661edf37bc1e32327fb2f79875f21a7 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Mon, 19 Oct 2026 15:24:54 +0700 Subject: [PATCH 145/318] fix(shader): link a table call whose V# is a slice of an s_load_dwordx8 or s_load_dwordx16 Co-Authored-By: Claude --- .../tests/execution/ComputeTableCalls.cpp | 14 ++++++++++++-- core/libs/tests/ScalarCall.cpp | 13 +++++++++++++ .../recompiler/ControlFlow/src/GraphBuilder.cpp | 6 ++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp index d9a6a8aa1d..f85c5c7d8a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeTableCalls.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "SceShaders.hpp" #include "VulkanTestDevice.hpp" +#include #include #include #include @@ -32,10 +33,14 @@ alignas(256) constexpr std::array HoistedCaller{ 0x7e020280u, 0xf4080400u, 0xfa000000u, 0xbf8cc07fu, 0xbf068203u, 0xbf850001u, 0x7e020280u, 0x8f148402u, 0xf4280208u, 0x28000000u, 0xbf8cc07fu, 0xbe8e0308u, 0xbe8f0309u, 0xbf068103u, 0xbe8e210eu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, 0xbf9f0000u, 0x30306c73u, 0x00000040u, 0x00000008u}; +alignas(256) constexpr std::array SliceCaller{ + 0x7e020280u, 0xf40c0600u, 0xfa000010u, 0x8f148402u, 0xbf8cc07fu, 0xf428020eu, 0x28000000u, 0xbf8cc07fu, + 0xbe8e0308u, 0xbe8f0309u, 0xbf068103u, 0xbe8e210eu, 0x34040082u, 0xe0701000u, 0x80010102u, 0xbf810000u, + 0xbf9f0000u, 0x30306c73u, 0x00000040u, 0x00000008u}; alignas(256) constexpr std::array SelectSixteen{0x85159091u, 0x7e020215u, 0xbe80200eu}; alignas(256) constexpr std::array SelectThirtyTwo{0x8515a0a1u, 0x7e020215u, 0xbe80200eu}; alignas(16) std::array Table{}; -alignas(16) std::array Descriptors{}; +alignas(16) std::array Descriptors{}; alignas(256) std::array Output{}; void Fill() { @@ -43,7 +48,10 @@ void Fill() { const auto second = reinterpret_cast(SelectThirtyTwo.data()); Table = {static_cast(first), static_cast(first >> 32u), 0u, 0u, static_cast(second), static_cast(second >> 32u), 0u, 0u}; const auto table = reinterpret_cast(Table.data()); - Descriptors = {static_cast(table), static_cast((table >> 32u) & 0xffffu) | (16u << 16u), 2u, 0x31016facu}; + const std::array descriptor{static_cast(table), static_cast((table >> 32u) & 0xffffu) | (16u << 16u), 2u, 0x31016facu}; + Descriptors = {}; + std::copy(descriptor.begin(), descriptor.end(), Descriptors.begin()); + std::copy(descriptor.begin(), descriptor.end(), Descriptors.begin() + 8); } std::array UserData(std::uint32_t index, bool scc) { @@ -149,6 +157,8 @@ int main() { Check(1u, true, wave32, 33u, "the second record's callee with SCC 1"); Check(0u, false, wave32, 16u, "the first record's callee through a descriptor loaded before a branch", HoistedCaller.data()); Check(1u, true, wave32, 33u, "the second record's callee through a descriptor loaded before a branch", HoistedCaller.data()); + Check(0u, false, wave32, 16u, "the first record's callee through the second half of an s_load_dwordx8", SliceCaller.data()); + Check(1u, true, wave32, 33u, "the second record's callee through the second half of an s_load_dwordx8", SliceCaller.data()); const auto log = Dispatch(2u, false, wave32); Require(log.find("called address 0x0 at program counter") != std::string::npos && log.find("which no linked callee of that call matches") != std::string::npos, std::string("a record past the table did not report an unlinked call") + (wave32 ? " (wave32)" : " (wave64)") + ": " + log); diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index c4d38b87c4..66216862ba 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include using namespace ShaderRecompiler; @@ -236,6 +237,18 @@ static void CheckTableCalls() { Require(table(10u, 0xbf810000u, 12u).empty()); Require(table(5u, 0x28000008u).size() == 1u && table(5u, 0x28000008u)[0].entryOffset == 8u); Require(table(0u, 0xbe940380u).empty()); + const auto slice = [&](std::uint32_t load, std::uint32_t bufferLoad) { + auto code = caller; + code[1] = load; + code[4] = bufferLoad; + return FindTableCalls(RdnaInstructionDecoder{}.Decode(code), swappc); + }; + for (const auto& [load, bufferLoad, offset] : {std::tuple{0xf40c0606u, 0xf428020cu, 0x270u}, std::tuple{0xf40c0606u, 0xf428020eu, 0x280u}, std::tuple{0xf4100606u, 0xf4280210u, 0x290u}, std::tuple{0xf4100606u, 0xf4280212u, 0x2a0u}}) { + const auto sliced = slice(load, bufferLoad); + Require(sliced.size() == 1u && sliced[0].userDataRegister == 12u && sliced[0].descriptorOffset == offset && sliced[0].entryOffset == 0u && sliced[0].entryStride == 16u); + } + Require(slice(0xf40c0606u, 0xf4280210u).empty()); + Require(slice(0xf40c0606u, 0xf428020du).empty()); auto readsLane = caller; readsLane.insert(readsLane.begin(), {0xd4c2000bu, 0x00020300u}); const auto readsLaneProgram = RdnaInstructionDecoder{}.Decode(readsLane); diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 5a2459f5af..45e66c5f65 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -1526,7 +1526,9 @@ std::vector FindTableCalls(const RdnaProgram& program, const SwappcIn if (!sameBlockWriter && !reachingWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize)) continue; const auto& table = program.instructions[descriptorLoad]; const auto pointer = scalarIndex(table.source0); - if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || pointer == NoScalarRegister || pointer % 2u != 0u || table.source1.kind != RdnaOperandKind::Null || + const auto loaded = scalarIndex(table.destination); + if ((table.op != RdnaOpcode::SLoadDwordx4 && table.op != RdnaOpcode::SLoadDwordx8 && table.op != RdnaOpcode::SLoadDwordx16) || loaded == NoScalarRegister || descriptor < loaded || + (descriptor - loaded) % 4u != 0u || descriptor - loaded + 4u > table.dataDwordCount || pointer == NoScalarRegister || pointer % 2u != 0u || table.source1.kind != RdnaOperandKind::Null || table.memoryOffset >= 0x100000u || (table.memoryOffset & 3u) != 0u) continue; std::optional constantPointer; if (!(userData(pointer) && userData(pointer + 1u) && unchangedUntil(program, pointer, descriptorLoad, swappc.waveSize) && unchangedUntil(program, pointer + 1u, descriptorLoad, swappc.waveSize))) { @@ -1548,7 +1550,7 @@ std::vector FindTableCalls(const RdnaProgram& program, const SwappcIn 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, constantPointer ? 0u : pointer, table.memoryOffset, load.memoryOffset + lowDword * 4u, stride, constantPointer}); + calls.push_back({instruction.programCounter, link, target, constantPointer ? 0u : pointer, table.memoryOffset + (descriptor - loaded) * 4u, load.memoryOffset + lowDword * 4u, stride, constantPointer}); } return calls; } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index c4ea080793..1fa1aeee34 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -199,7 +199,7 @@ - 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 (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. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. 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`, or a 4-dword-aligned slice of an `s_load_dwordx8` or `s_load_dwordx16`, 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. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. 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. - [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. From 73c9c2a7628a563f1b0785914f16e43ae8ba3597 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Tue, 20 Oct 2026 15:26:38 +0700 Subject: [PATCH 146/318] test(shader): reject misaligned and partial V# slices of a table call's s_load_dwordx8 Co-Authored-By: Claude --- core/libs/tests/ScalarCall.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index 66216862ba..2da9bd35ef 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -249,6 +249,9 @@ static void CheckTableCalls() { } Require(slice(0xf40c0606u, 0xf4280210u).empty()); Require(slice(0xf40c0606u, 0xf428020du).empty()); + Require(slice(0xf40c0686u, 0xf428020eu).empty()); + Require(slice(0xf40c0686u, 0xf428020cu).empty()); + Require(slice(0xf40c0586u, 0xf428020eu).empty()); auto readsLane = caller; readsLane.insert(readsLane.begin(), {0xd4c2000bu, 0x00020300u}); const auto readsLaneProgram = RdnaInstructionDecoder{}.Decode(readsLane); From 99493bb59081284ea106a1575ad45379a4cba986 Mon Sep 17 00:00:00 2001 From: Bram910 Date: Wed, 21 Oct 2026 19:18:22 +0200 Subject: [PATCH 147/318] fix(relinker): validate programmatic patch widths --- .../relinker/src/patching/GamePatch.cpp | 21 +++++-- .../relinker/tests/GamePatchTests.cpp | 57 +++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/core/relinker/relinker/src/patching/GamePatch.cpp b/core/relinker/relinker/src/patching/GamePatch.cpp index 05d334868a..1945eb159b 100644 --- a/core/relinker/relinker/src/patching/GamePatch.cpp +++ b/core/relinker/relinker/src/patching/GamePatch.cpp @@ -240,6 +240,15 @@ PatchEntry BuildEntry(const Table& table, const std::size_t headerLine, std::set return entry; } +std::string Describe(const PatchEntry& entry) { + return "patch " + entry.Target + " va=" + HexValue(entry.VirtualAddress) + " (line " + std::to_string(entry.Line) + ")"; +} + +void ValidateWidth(const PatchEntry& entry) { + if (entry.Expected.empty() || entry.Expected.size() != entry.Value.size()) + throw Domain::RelinkerException(Describe(entry) + ": expected and value must have the same nonzero width"); +} + struct EntryRange { std::optional Range; std::string Reason; @@ -275,14 +284,13 @@ std::vector ResolveEach(const ElfReader& reader, const std::vector

resolved; - for (const auto& entry : entries) resolved.push_back(ResolveEntry(segments, reader.GetFileSize(), header, entry)); + for (const auto& entry : entries) { + ValidateWidth(entry); + resolved.push_back(ResolveEntry(segments, reader.GetFileSize(), header, entry)); + } return resolved; } -std::string Describe(const PatchEntry& entry) { - return "patch " + entry.Target + " va=" + HexValue(entry.VirtualAddress) + " (line " + std::to_string(entry.Line) + ")"; -} - bool Overlaps(const std::uint64_t firstOffset, const std::uint64_t firstLength, const std::uint64_t secondOffset, const std::uint64_t secondLength) { return firstOffset < secondOffset + secondLength && secondOffset < firstOffset + firstLength; } @@ -427,7 +435,10 @@ std::vector ResolveRanges(const ElfReader& reader, const std::vect std::vector ApplyPatches(std::vector& bytes, const std::string& target, const std::vector& entries, const std::vector& ranges) { if (entries.size() != ranges.size()) throw Domain::RelinkerException("Patch ranges do not match patch entries"); for (std::size_t index = 0; index < entries.size(); ++index) { + ValidateWidth(entries[index]); const auto& range = ranges[index]; + if (range.Length != entries[index].Expected.size()) + throw Domain::RelinkerException(Describe(entries[index]) + ": range length does not match the patch width", range.Offset); if (range.Offset > bytes.size() || range.Length > bytes.size() - range.Offset) throw Domain::RelinkerException(Describe(entries[index]) + ": range exceeds the image", range.Offset); const auto first = bytes.begin() + static_cast(range.Offset); if (!std::equal(entries[index].Expected.begin(), entries[index].Expected.end(), first)) diff --git a/core/relinker/relinker/tests/GamePatchTests.cpp b/core/relinker/relinker/tests/GamePatchTests.cpp index f81bc1f2fe..1ed801fe45 100644 --- a/core/relinker/relinker/tests/GamePatchTests.cpp +++ b/core/relinker/relinker/tests/GamePatchTests.cpp @@ -282,6 +282,54 @@ void applyAndMismatch() { require(PatchTarget(bytes, "other", entries).Applied.empty() && bytes == original, "entry hit another target"); } +void programmaticWidths() { + auto bytes = twoSegmentFixture(); + const auto original = bytes; + const ElfReader reader(original); + const std::vector invalid{ + entry("t", 0x1000, {0}, {9, 8}), + entry("t", 0x1000, {0, 1}, {9}), + entry("t", 0x1000, {}, {}), + entry("t", 0x1000, {}, {9}), + entry("t", 0x1000, {0}, {}), + }; + for (const auto& patch : invalid) { + const std::vector entries{entry("t", 0x1100, {0x80}, {1}), patch}; + rejects([&] { ResolveRanges(reader, entries); }, "same nonzero width"); + rejects([&] { PatchTarget(bytes, "t", entries); }, "same nonzero width"); + require(bytes == original, "an invalid patch width wrote bytes"); + rejects([&] { ApplyPatches(bytes, "t", entries, {{0x2000, 1, 0x1100}, {0x1000, patch.Expected.size(), 0x1000}}); }, "same nonzero width"); + require(bytes == original, "direct application of an invalid patch width wrote bytes"); + for (const bool outputMode : {false, true}) { + PatchFile file; + file.MainTarget = "t"; + file.Entries = entries; + rejects([&] { VerifyPatches(file, {{"t", original}}, {}, outputMode); }, "same nonzero width"); + } + } +} + +void applicationRangeWidths() { + const std::vector original{0, 1, 2, 3}; + auto bytes = original; + const std::vector entries{entry("t", 0x1000, {0}, {9}), entry("t", 0x1002, {2, 3}, {8, 7})}; + for (const std::uint64_t length : {0u, 1u, 3u}) { + rejects([&] { ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}, {2, length, 0x1002}}); }, "range length does not match"); + require(bytes == original, "a mismatched range length wrote bytes"); + } + rejects([&] { ApplyPatches(bytes, "t", {entry("t", 0x1003, {3, 4}, {8, 7})}, {{3, 1, 0x1003}}); }, "range length does not match"); + require(bytes == original, "a truncated comparison range wrote bytes"); + for (const std::uint64_t offset : std::array{3, 4, 5, UINT64_MAX}) { + rejects([&] { ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}, {offset, 2, 0x1002}}); }, "range exceeds the image"); + require(bytes == original, "an out-of-bounds patch wrote bytes"); + } + rejects([&] { ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}}); }, "ranges do not match"); + require(bytes == original, "a missing range wrote bytes"); + const auto applied = ApplyPatches(bytes, "t", entries, {{0, 1, 0x1000}, {2, 2, 0x1002}}); + require(bytes == std::vector({9, 1, 8, 7}) && applied.size() == 2, "valid boundary patches failed"); + require(ApplyPatches(bytes, "t", {}, {}).empty(), "empty patch list was rejected"); +} + void overlapWithWriters() { const std::vector patched{{0x1010, 2, 0x401010}}; const auto reject = [&](const WriterSites& sites, const std::string& writer) { rejects([&] { CheckOverlaps("t", patched, sites); }, writer); }; @@ -340,6 +388,13 @@ void conversion() { rejects([&] { converter.Convert(conversionInput(executable), failing); }, "expected 07 found 00"); failing.Patches = {entry("custom-name.bin", 0x1000, {0xc3, 0}, {0, 0}), entry("custom-name.bin", 0x1001, {0}, {1})}; rejects([&] { converter.Convert(conversionInput(executable), failing); }, "overlaps"); + for (const auto target : {TargetSystem::Linux, TargetSystem::Windows, TargetSystem::MacOs}) { + failing.Target = target; + for (const char* name : {"custom-name.bin", "sce_module/plugin.prx"}) { + failing.Patches = {entry(name, 0x1001, {0}, {9, 8})}; + rejects([&] { converter.Convert(conversionInput(executable), failing); }, "same nonzero width"); + } + } const auto repeated = converter.Convert(conversionInput(executable), plain); require(repeated.Executable == baseline.Executable && repeated.Modules.front().Bytes == baseline.Modules.front().Bytes, "no entries changed the output"); @@ -511,6 +566,8 @@ int main() { ranges(); headerRange(); applyAndMismatch(); + programmaticWidths(); + applicationRangeWidths(); overlapWithWriters(); conversion(); targets(); From 64675a3756f3614ab33bf7664704f25183d9ca21 Mon Sep 17 00:00:00 2001 From: klosixgaming-arch Date: Wed, 21 Oct 2026 17:22:39 +0000 Subject: [PATCH 148/318] fix(shader): require the phi arm table offset to dominate the table read When a table column pointer is read through a phi, the offset taken from one arm is substituted into the image handle after the merge. An offset computed inside an arm does not dominate that use, so planning now refuses it instead of producing an invalid module. Adds a case where each arm computes its own offset; it plans as a table on the previous head and is refused with this change. AI-assisted: yes. Written with AI assistance and reviewed by me. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 9 +++++++ .../Optimization/src/ResourceTracker.cpp | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 058046a306..cc24a594d9 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3487,6 +3487,15 @@ void verifyPointerChainImageTable() { auto nonTableRequest = TableRequest(nonTableCode, userData); nonTableRequest.shader.codeAddress = 0x3a000u; expectFailure([&] { static_cast(GetResourcePlan(nonTableRequest)); }, "GetImageResource dword 0 is not a valid runtime value", "pointer chain image table: a phi with a non-table branch was planned as a table"); + + auto armOffsetCode = code; + armOffsetCode.erase(armOffsetCode.begin() + 14, armOffsetCode.begin() + 17); + armOffsetCode.insert(armOffsetCode.begin() + 14, { + 0xbf068010u, 0xbf850004u, 0x8f128411u, 0xf4240b02u, 0x24000008u, 0xbf820003u, 0x8f128411u, 0xf4240b02u, 0x24000008u + }); + auto armOffsetRequest = TableRequest(armOffsetCode, userData); + armOffsetRequest.shader.codeAddress = 0x3b000u; + expectFailure([&] { static_cast(GetResourcePlan(armOffsetRequest)); }, "GetImageResource dword 0 is not a valid runtime value", "pointer chain image table: a phi whose arms compute their own table offset was planned as a table"); } void verifyDescriptorPhis() { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index ceb8017633..04d2be4845 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -732,6 +732,27 @@ private: return candidate; } + bool BlockDominates(const IrBlock* dominator, const IrBlock* block) const { + if (dominator == block) return true; + std::vector pending{block}; + std::unordered_set visited{block}; + while (!pending.empty()) { + const IrBlock* current = pending.back(); + pending.pop_back(); + if (current->Predecessors().empty()) return false; + for (const IrBlock* predecessor : current->Predecessors()) { + if (predecessor != dominator && visited.insert(predecessor).second) pending.push_back(predecessor); + } + } + return true; + } + + bool AvailableAt(IrValue* value, const IrBlock* block) const { + value = value->Resolve(); + if (value->HasImmediate() || value->Parent() == nullptr) return true; + return BlockDominates(value->Parent(), block); + } + bool TryMakeIndirectColumn(IrValue& handle, IrValue& descriptorPointer, bool sampler, std::uint32_t words, std::uint32_t descriptorOffset, std::uint32_t descriptorImmediate, TableColumnPlan& plan) { std::vector hops; IrValue* pointer = &descriptorPointer; @@ -786,6 +807,9 @@ private: return false; } } + if (!AvailableAt(offset, handle.Parent())) { + return false; + } IrValue* key = nullptr; std::uint32_t stride = 0; std::uint32_t addend = 0; From 0b5ba4470391f61f53e8e0283fcd8663223c65e8 Mon Sep 17 00:00:00 2001 From: krishalivaibhav Date: Fri, 16 Oct 2026 14:51:53 +0530 Subject: [PATCH 149/318] fix(agc): synchronize pending writes before command allocation --- core/libs/prx/libSceAgc/CMakeLists.txt | 16 ++++++++-------- core/libs/prx/libSceAgc/Command/src/Packet.cpp | 2 ++ core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +++++++ .../Execution/include/Driver.hpp | 2 ++ .../Execution/src/Driver/EntryPoints.cpp | 6 ++++++ .../libs/prx/libSceAgcDriver/tests/Recorder.cpp | 17 +++++++++++++++++ 6 files changed, 42 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index faf498b571..509369d73b 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -157,7 +157,7 @@ add_test_executable(agc_register_command_tests EXCLUDE_FROM_ALL Patch/src/IndirectRegisters.cpp ) target_include_directories(agc_register_command_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_register_command_tests PRIVATE libc) +target_link_libraries(agc_register_command_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_register_command_tests) if(BUILD_TESTING) set_target_properties(agc_register_command_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -176,7 +176,7 @@ add_test_executable(agc_cb_cond_write_tests EXCLUDE_FROM_ALL Cb/src/Branch.cpp ) target_include_directories(agc_cb_cond_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_cb_cond_write_tests PRIVATE libc) +target_link_libraries(agc_cb_cond_write_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_cb_cond_write_tests) if(BUILD_TESTING) set_target_properties(agc_cb_cond_write_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -220,7 +220,7 @@ add_test_executable(agc_branch_patch_tests EXCLUDE_FROM_ALL Patch/src/Branch.cpp ) target_include_directories(agc_branch_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_branch_patch_tests PRIVATE libc) +target_link_libraries(agc_branch_patch_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_branch_patch_tests) if(BUILD_TESTING) set_target_properties(agc_branch_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -240,7 +240,7 @@ add_test_executable(agc_predication_tests EXCLUDE_FROM_ALL Predication/src/Predication.cpp ) target_include_directories(agc_predication_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_predication_tests PRIVATE libc) +target_link_libraries(agc_predication_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_predication_tests) if(BUILD_TESTING) set_target_properties(agc_predication_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -357,7 +357,7 @@ add_test_executable(agc_dma_data_tests EXCLUDE_FROM_ALL DcbFlow/src/Memory.cpp ) target_include_directories(agc_dma_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_dma_data_tests PRIVATE libc) +target_link_libraries(agc_dma_data_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_dma_data_tests) if(BUILD_TESTING) set_target_properties(agc_dma_data_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -377,7 +377,7 @@ add_test_executable(agc_write_data_patch_tests EXCLUDE_FROM_ALL Patch/src/WaitRegMem.cpp ) target_include_directories(agc_write_data_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_write_data_patch_tests PRIVATE libc) +target_link_libraries(agc_write_data_patch_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_write_data_patch_tests) if(BUILD_TESTING) set_target_properties(agc_write_data_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -397,7 +397,7 @@ add_test_executable(agc_prime_utcl2_tests EXCLUDE_FROM_ALL DcbState/src/Display.cpp ) target_include_directories(agc_prime_utcl2_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_prime_utcl2_tests PRIVATE libc) +target_link_libraries(agc_prime_utcl2_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_prime_utcl2_tests) if(BUILD_TESTING) set_target_properties(agc_prime_utcl2_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) @@ -418,7 +418,7 @@ add_test_executable(agc_cond_exec_patch_tests EXCLUDE_FROM_ALL Patch/src/CondExec.cpp ) target_include_directories(agc_cond_exec_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(agc_cond_exec_patch_tests PRIVATE libc) +target_link_libraries(agc_cond_exec_patch_tests PRIVATE libSceAgcDriver libc) configure_windows_unwind(agc_cond_exec_patch_tests) if(BUILD_TESTING) set_target_properties(agc_cond_exec_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index 6ec0e12165..be43fb7028 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgc/Command/include/Packet.hpp" #include @@ -67,6 +68,7 @@ void Reserve(CommandBuffer* buffer, std::uint32_t count, const char* function) { std::uint32_t* Allocate(CommandBuffer* buffer, std::uint32_t count, const char* function) { Reserve(buffer, count, function); auto* result = buffer->cursor_up; + AgcDriverPrepareCommandWrite_nid_no_patch(reinterpret_cast(result), static_cast(count) * sizeof(std::uint32_t)); buffer->cursor_up += count; return result; } diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index d4f1ad05ce..5ccfb92cd3 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1403,6 +1403,7 @@ target_include_directories(agc_driver_recorder_tests PRIVATE ) target_link_libraries(agc_driver_recorder_tests PRIVATE + libSceAgc libkernel libSceAgcDriver SDL2-static @@ -1422,6 +1423,12 @@ if(BUILD_TESTING) set_tests_properties(agc_recorder_device_selection agc_gpu_timing_complete agc_gpu_timing_incomplete PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_command_memory_reuse COMMAND agc_driver_recorder_tests --command-reuse-only) + set_tests_properties(agc_command_memory_reuse PROPERTIES SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_command_memory_reuse PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_pipeline_cache COMMAND agc_driver_recorder_tests --pipeline-cache-only) add_test(NAME agc_pipeline_cache_file COMMAND agc_driver_recorder_tests --pipeline-cache-file-only) add_test(NAME agc_pipeline_library COMMAND agc_driver_recorder_tests --pipeline-library-only) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index b599b88f95..384c601ccc 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -3,6 +3,7 @@ #include #include +#include #include "SceTypes.hpp" #include "SceShaders.hpp" @@ -12,6 +13,7 @@ void Submit(const Packet* packet, std::uint32_t queue); } +extern "C" void AgcDriverPrepareCommandWrite_nid_no_patch(std::uint64_t address, std::size_t bytes); extern "C" void AgcDriverWaitIdle_nid_postfix(); extern "C" void AgcDriverRaiseWorkerThreadPriority_nid_postfix(const char* role); extern "C" void AgcDriverLockVulkanLoader_nid_postfix(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 04a1f72a02..8e846cf2fa 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,3 +1,4 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" @@ -160,3 +161,8 @@ extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(block); + CommandBuffer disjoint{commandWords + 12, commandWords + 16, commandWords + 12, nullptr, nullptr, nullptr, 0}; + Agc::Command::Emit(&disjoint, 0x7a, {0x20000243, 0x400}, __func__); + Require(recorder.Recording(), "a disjoint command allocation drained the GPU store"); + CommandBuffer reused{commandWords + 6, commandWords + 10, commandWords + 6, nullptr, nullptr, nullptr, 0}; + Agc::Command::Emit(&reused, 0x7a, {0x20000243, 0x400}, __func__); + recorder.Sync(); + Require(words[8] == 0x400, "a pending GPU label overwrote a reused command word"); // The covered-access mask: reported once by the next Commands() call, then cleared. VkAccessFlags covered = 0xffffffffu; recorder.Commands(&covered); @@ -10138,6 +10149,12 @@ int main(int argc, char** argv) { std::cout << "host imports " << (PrepareImportWatch(device.GetContext()) == ImportWatch::Unwatch ? "are compared" : "stay watched") << '\n'; Recorder recorder(device.GetContext()); recorder.Activate(); + if (argc == 2 && std::string_view(argv[1]) == "--command-reuse-only") { + if (device.GetContext().hostImportAlignment == 0) return 77; + const auto before = Recorder::StoreCounts().stores; + storeRunTests(device, recorder); + return Recorder::StoreCounts().stores != before ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--copied-direct-only") { copiedDirectOrderingTests(device, recorder); return 0; From 6de0666ce57a030e6b36729b894a7568cb2e9602 Mon Sep 17 00:00:00 2001 From: Johan David Date: Mon, 19 Oct 2026 16:19:34 -0500 Subject: [PATCH 150/318] perf(agc): read a sampled texture's DCC keys once per lookup cachedTexture read the keys of one range up to three times in a lookup with nothing written in between: the ClearSingle check over a pending image dropped its scan, the staged-clear test scanned even without a pending image and kept nothing, and the final read scanned again. On Windows the key ranges sit in compared host imports, so no proof stands in for these scans; in PPSA31327's gameplay they took 12 % of the draw committer's samples (#8068). The lookup now keeps the keys it read until it does something that can change them: the storage source it looks up and may refresh, the pending source's Refresh, or a pending flush, which can publish unit shadows over the surface even when it flushes no image. The staged-clear test reads the keys only with a pending source. Which keys a lookup acts on is unchanged; only repeated reads with nothing between them are gone. DccKeyPresync checks that a dispatch's lookup with no key writer reads the keys at most once under the GPU mutex; main reads them twice. --- .../Graphics/src/ShaderResources.cpp | 18 +++++++++++----- .../tests/execution/DccKeyPresync.cpp | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d6d84cd4aa..43178ec182 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -720,11 +720,13 @@ std::shared_ptr cachedTexture(const Context& context, std::span scanned; const auto scanKeys = [&] { + if (scanned.has_value()) return *scanned; const auto scanStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - const auto keys = TextureClearKeys(resource, guestBytes); + scanned = TextureClearKeys(resource, guestBytes); if (profile && resource.dccAddress != 0) LookupOutcomes::Add(LookupOutcomes::DccScan, scanStart); - return keys; + return *scanned; }; auto& counters = TextureCounts(); const auto address = resource.baseAddress; @@ -780,6 +782,7 @@ std::shared_ptr cachedTexture(const Context& context, std::spanDescriptor().dccAddress == resource.dccAddress) source = std::move(candidate); } + scanned.reset(); // The lookup's Refresh may have flushed another image's results over the memory (which // marks the keys uncompressed): a surface that stays a snapshot re-reads them behind that // flush, as it does behind its own. APS5_NO_KEYS_RESCAN=1 keeps the keys scanned above. @@ -795,15 +798,19 @@ std::shared_ptr cachedTexture(const Context& context, std::spanGenerationFor(address, bytes))) { // The CPU wrote the memory while results were pending: Refresh merges them the usual way. refreshForView(*source, resource); + scanned.reset(); } // Results stored to guest memory just now are stamped newer than `generation`; a snapshot read // after them is current at the generation of a second (memoized) collect. The store marks the // surface's DCC keys uncompressed, so the keys are read after it. const auto flushStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - const auto observedKeys = keys.has_value() ? *keys : scanKeys(); - const bool stagedClear = source != nullptr && resource.dccAddress != 0 && (IsDccClear(observedKeys) || observedKeys == DccKeys::ClearSingle); + const bool stagedClear = source != nullptr && resource.dccAddress != 0 && [&] { + const auto observedKeys = keys.has_value() ? *keys : scanKeys(); + return IsDccClear(observedKeys) || observedKeys == DccKeys::ClearSingle; + }(); bool stagedFlushed = false; - if ((source == nullptr || stagedClear) && !clearThroughKeys && StorageTexture::FlushPending(address, bytes, nullptr, "sampled texture")) { + const bool flushAsked = (source == nullptr || stagedClear) && !clearThroughKeys; + if (flushAsked && StorageTexture::FlushPending(address, bytes, nullptr, "sampled texture")) { if (profile) LookupOutcomes::Add(LookupOutcomes::PendingFlush, flushStart); generation = GuestMemory::CollectWrites(address, bytes); if (stagedClear) { @@ -814,6 +821,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(SampleSecondTexelCode.data())); + const auto waits = AgcDriver::Graphics::KeyWaitCounts(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data()), std::move(prepared)); + device.WaitIdle(); + } + const auto reads = AgcDriver::Graphics::KeyWaitCounts().calls - waits.calls; + char message[192]; + std::snprintf(message, sizeof(message), "%s read the keys of its sampled texture %llu times under the GPU mutex with nothing written between the reads, expected at most once", what, static_cast(reads)); + Require(reads <= 1u, message); +} + void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData); const auto before = AgcDriver::VulkanDevice::PresyncCounts(); @@ -273,6 +289,11 @@ int main() { RunLockedSync(*device, control); RunPresync(*device, changed); RunNoWriter(*device, changed); + GuestBlock single; + GuestBlock singleOutput; + const auto reads = Prepare(single.Data(), singleOutput.Data()); + RunOneKeyReadPerLookup(*device, reads, "a dispatch's first lookup"); + RunOneKeyReadPerLookup(*device, reads, "a dispatch's repeated lookup"); RunKnownKeys(*device, fills); std::puts("DCC key pre-sync tests passed"); return 0; From 2e7494bbab8311610b8cb8dc8afbada1e753a493 Mon Sep 17 00:00:00 2001 From: Johan David Date: Wed, 21 Oct 2026 00:38:29 -0500 Subject: [PATCH 151/318] test(agc): count only the lookup's own key scans in DccKeyPresync RunOneKeyReadPerLookup compared the process-wide KeyWaitCounts().calls around a dispatch. The repeated lookup sometimes refreshes the surface's storage image, and its upload and ProvedKeys read the keys three more times on the same thread, so the test failed in about one run of four. cachedTexture now counts the key scans it makes on the calling thread (SampledLookupKeyScans()), and the test compares that count. --- .../libSceAgcDriver/Graphics/include/ShaderResources.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 7 +++++++ .../prx/libSceAgcDriver/tests/execution/DccKeyPresync.cpp | 8 ++++---- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 5321104605..cd2d3deb23 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -146,6 +146,7 @@ struct ImageTableReuseCounts { std::uint64_t writesMade = 0; }; ImageTableReuseCounts ImageTableReuses(); +std::uint64_t SampledLookupKeyScans(); struct PresyncSurface { std::uint64_t address; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 43178ec182..9394e9cc41 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -394,6 +394,8 @@ TextureCounters& TextureCounts() { return counters; } +thread_local std::uint64_t sampledLookupKeyScans = 0; + void reportTextureCounters() { static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; if (!profile) return; @@ -725,6 +727,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(SampleSecondTexelCode.data())); - const auto waits = AgcDriver::Graphics::KeyWaitCounts(); + const auto before = AgcDriver::Graphics::SampledLookupKeyScans(); { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast(SampleSecondTexelCode.data()), std::move(prepared)); device.WaitIdle(); } - const auto reads = AgcDriver::Graphics::KeyWaitCounts().calls - waits.calls; + const auto scans = AgcDriver::Graphics::SampledLookupKeyScans() - before; char message[192]; - std::snprintf(message, sizeof(message), "%s read the keys of its sampled texture %llu times under the GPU mutex with nothing written between the reads, expected at most once", what, static_cast(reads)); - Require(reads <= 1u, message); + std::snprintf(message, sizeof(message), "%s scanned the keys of its sampled texture %llu times with nothing written between the scans, expected at most once", what, static_cast(scans)); + Require(scans <= 1u, message); } void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) { From e3c8b51ac287d89aae11cfded9eb0815c7009600 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 22 Oct 2026 00:32:57 +0400 Subject: [PATCH 152/318] docs(debt): list the GPUs the rect-list barycentric test passed on --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e93aa7a9e1..def0a04455 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -96,7 +96,7 @@ - [Mesh layer output](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) (shader recompiler) - a mesh shader writes each primitive's layer through a one-member `Block` struct array whose member is decorated `BuiltIn Layer` and `PerPrimitiveEXT`, as glslang declares `gl_Layer` inside `gl_MeshPerPrimitiveEXT`, not through a standalone `PerPrimitiveEXT` `Layer` array. Both forms pass `spirv-val`, but AMD's Windows driver (32.0.21045.5002, RX 6700 XT) crashes in `amdvlk64.dll` creating a pipeline whose mesh shader writes the standalone form, whether the layer is signed or unsigned and whatever the target, while the block form draws, also with a pixel shader that reads the layer. - [Shader recompilation](../../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../../core/shader/recompiler) was written completely independently from [libs/prx](../../../core/libs/prx). - [v_interp_p1ll_f16, v_interp_p1lv_f16 and v_interp_p2_f16](../../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp) read the 16-bit halves of each vertex's parameter through `VK_KHR_fragment_shader_barycentric`, as fixed-function interpolation cannot split a 32-bit attribute. Without the extension the pixel shader takes the [barycentric emulation](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), whose generated geometry stage passes each vertex's parameter to it as a flat input, so the halves arrive bit for bit; a device without geometry shaders takes the tessellation stages of the same emulation instead, which no 16-bit interpolation test has run through yet. P10 and P20 are the vertex differences rounded to f16 to nearest even, which is not confirmed on hardware. v_interp_p1ll_f16 and v_interp_p1lv_f16 round the exact `P10 * I + P0` once to f32 and v_interp_p2_f16 rounds the exact `P20 * J + S2` once to f16, as measured on RDNA2 (#697); the exact sums are computed in f64 or, on a device without the Float64 capability, in 64-bit integers: the exact product is added with a sticky bit and rounded once. They follow the shader's float mode, measured on RDNA2 (gfx1035) in all eight combinations of `IEEE_MODE` and kept/flushed f32 and f16 denormals over 768 special and 1,024 random rows. With IEEE=1 a NaN operand is returned quieted; with IEEE=0 it is returned unquieted, except that v_interp_p2_f16's f16 result is always quiet. Flushed f32 denormals flush I/J, v_interp_p2_f16's f32 S2 and the f32 result; flushed f16 denormals flush P0, P10/P20, v_interp_p1lv_f16's f16 S2 and the f16 result. Denormal modes 1 and 2 (flush only inputs or only outputs) and rounding other than to nearest even are not measured and throw. An unknown mode follows IEEE=0 with denormals kept. Lavapipe has no barycentrics, so the draw test is skipped in CI; `agc_driver_interpolation_modes` checks the integer arithmetic on every device, and the f64 arithmetic on devices with Float64. It checks both against the hardware rows in every mode, and against a CPU reference on random rows in every mode and f16 rounding. -- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the provoking vertex's value there, the first or with `PROVOKING_VTX_LAST` the last, and every corner of the patch writes it, so the host's own provoking vertex does not matter and `VK_EXT_provoking_vertex` is not needed; a flat per-vertex parameter with the last provoking vertex still throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. A rect-list draw takes them from its own generated tessellation stages, checked on MoltenVK 1.4.2 (Apple M2) only. Tessellation and mesh pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. +- Without `VK_KHR_fragment_shader_barycentric`, a pixel shader whose I/J VGPRs [only feed `v_interp_p1_f32`/`v_interp_p2_f32` of their own pair](../../../core/shader/recompiler/Optimization/src/HostInterpolationChecker.cpp) reads the attribute Vulkan interpolated. Any other read of them or of a raw vertex parameter takes the weights and the three vertex values from a [generated geometry stage](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp), which needs `geometryShader` and a vertex shader before it. On a device with `tessellationShader` but no `geometryShader` (MoltenVK), a triangle list without primitive restart takes them from [generated tessellation stages](../../../core/shader/recompiler/SpirvBackend/src/BarycentricGeometryShader.cpp) instead: each triangle is one three-point patch at tessellation level 1, the evaluation stage writes the corner `gl_TessCoord` points at as the weights, and the vertex clip distances are forwarded: the control stage reads them as the separate `ClipDistance` array the vertex shader writes, so the vertex-to-control interfaces match, and passes them on inside its per-vertex block, which the evaluation stage reads and writes. MoltenVK 1.4.2 (SPIRV-Cross) cannot translate a separate `ClipDistance` array in a tessellation evaluation stage (`no member named 'gl_ClipDistance' in 'main0_out'`), so the two generated stages keep it in their blocks. A flat parameter takes the provoking vertex's value there, the first or with `PROVOKING_VTX_LAST` the last, and every corner of the patch writes it, so the host's own provoking vertex does not matter and `VK_EXT_provoking_vertex` is not needed; a flat per-vertex parameter with the last provoking vertex still throws. The patch keeps the triangle's facing with `VertexOrderCw`, checked with face culling on MoltenVK 1.4.2 (Apple M2) only. A rect-list draw takes them from its own generated tessellation stages; its test passes on MoltenVK 1.4.2 (Apple M2), RADV (Radeon RX 7900 XT, Mesa 25.2.8), lavapipe, and on Windows on an RTX 3080 (NVIDIA 617.42) and Radeon RDNA 2 iGPUs (AMD 32.0.21045 and 32.0.21030); no title was run through it except Ruffle PPSA68091 on the M2. Tessellation and mesh pipelines, primitives other than triangles, triangle strips and fans without `geometryShader`, more than 32 fragment input locations and interfaces beyond the device's geometry or tessellation limits throw. - [v_fma_f64 and v_div_fmas_f64](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) with a subnormal operand or result round once, as IEEE 754 fusedMultiplyAdd does with f64 denormals kept, measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1, `v_div_fmas_f64` with VCC clear and set. The f16/f64 denormal field of `FLOAT_MODE` (bits 7:6) is not read: when it flushes, the hardware flushes the operands and the result of `v_fma_f64` (tininess before rounding) and only the result of `v_div_fmas_f64`, while these keep them. Shaders with f64 instructions declare `DenormPreserve` for 64-bit floats only when `shaderDenormPreserveFloat64` is true and Vulkan reports `denormBehaviorIndependence` as `ALL`; other independence modes are not handled by the current backend because they require coupled execution modes. See [VkShaderFloatControlsIndependence](https://docs.vulkan.org/refpages/latest/refpages/source/VkShaderFloatControlsIndependence.html). Elsewhere (including lavapipe), the driver may flush f64 subnormals, and `agc_driver_f64_fma_subnormals` is skipped. - [v_floor_f64, v_ceil_f64, v_fract_f64, v_frexp_mant_f64 and v_frexp_exp_i32_f64](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field: a denormal input is flushed to a zero of its sign when bit 0 is clear (0 and 2), and `v_fract_f64` flushes a denormal result, the fraction of a positive denormal input, to +0 when bit 1 is clear (0 and 1). `v_trunc_f64` and `v_rndne_f64` give the same results either way and are unchanged. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3 over 23 f64 values (zeros, denormals, minimum normals, ±0.5, ±1, values next to 1, the largest finite value, infinities and quiet and signaling NaNs) and 16 of them with neg and abs modifiers. Shaders without a known float mode keep the previous lowering. The PS5's gfx1013 is not measured. - [v_add_f64 and v_mul_f64 denormals](../../../core/shader/recompiler/Translation/src/Float64Instructions.cpp) follow the `FLOAT_MODE` f16/f64 denormal field; the PS5's gfx1013 is not measured. Inputs flush to a zero of their sign when bit 0 is clear (0 and 2). When bit 1 is clear (0 and 1), a sum below 2^-1022, or a product whose exact value rounded to 53 significant bits with an unbounded exponent is below 2^-1022, flushes to a zero of its sign, so `0x2000000000000001 * 0x1ffffffffffffffe` (exactly 2^-1022 - 2^-1126) rounds to 2^-1022 and is kept. Measured on RDNA2 (gfx1031, RX 6700 XT, AMD Windows driver 32.0.21045.5002, HIP) at IEEE=0 and 1, denorm16 0..3, over 576 pairs of 24 f64 values (zeros, denormals, minimum normals, values next to 1 and to 2^-1022, products just below 2^-1022, the largest finite value, infinities), with neg, abs and clamp. `v_fma_f64`, `v_div_fmas_f64` and shaders without a known float mode keep the previous lowering. From baf01b724eb3a285980fbbb74c7b8cb10d80827c Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:01:13 +0300 Subject: [PATCH 153/318] perf(shader): mark GetSrtResource and ReadConst as recomputable in RVE --- .../src/RedundantValueEliminator.cpp | 79 +++++++++++++++---- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp index 6e8c4e269d..5244bf70f9 100644 --- a/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp +++ b/core/shader/recompiler/Optimization/src/RedundantValueEliminator.cpp @@ -122,12 +122,21 @@ bool Recomputable(IrOpcode opcode) { case IrOpcode::FPTrunc32: case IrOpcode::FPFract32: case IrOpcode::FPRoundEven32: + case IrOpcode::GetSrtResource: + case IrOpcode::ReadConst: return true; default: return false; } } +bool ClobbersReadConst(IrOpcode opcode) { + const auto buffer = BufferAccessOf(opcode); + if (buffer == BufferAccess::Write || buffer == BufferAccess::Atomic) return true; + const auto address = AddressOpcodeInfoOf(opcode).access; + return address == AddressAccess::Write || address == AddressAccess::Atomic || opcode == IrOpcode::Barrier; +} + struct Operand { const IrValue* value; IrType type; @@ -145,11 +154,12 @@ struct ValueKey { IrOpcode opcode; IrType type; std::uint64_t flags; + std::uint64_t memoryEpoch = 0; std::array operands{}; std::size_t operandCount = 0; bool operator==(const ValueKey& other) const { - if (opcode != other.opcode || type != other.type || flags != other.flags || operandCount != other.operandCount) return false; + if (opcode != other.opcode || type != other.type || flags != other.flags || memoryEpoch != other.memoryEpoch || operandCount != other.operandCount) return false; for (std::size_t index = 0; index < operandCount; ++index) { if (operands[index] != other.operands[index] && OperandOf(operands[index]) != OperandOf(other.operands[index])) return false; } @@ -159,7 +169,7 @@ struct ValueKey { struct ValueKeyHash { std::size_t operator()(const ValueKey& key) const { - std::size_t hash = static_cast(key.opcode) * 0x9e3779b97f4a7c15ull ^ static_cast(key.type) ^ std::hash{}(key.flags) * 31u; + std::size_t hash = static_cast(key.opcode) * 0x9e3779b97f4a7c15ull ^ static_cast(key.type) ^ std::hash{}(key.flags) * 31u ^ std::hash{}(key.memoryEpoch) * 131u; for (const auto* argument : std::span(key.operands).first(key.operandCount)) { const auto operand = OperandOf(argument); hash = hash * 1099511628211ull ^ std::hash{}(operand.value) ^ std::hash{}(operand.immediate) * 7u ^ static_cast(operand.type); @@ -222,13 +232,61 @@ std::vector> DominatorChildren(const std::vector RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& program) const { RedundantValueEliminationStats stats; + if (program.BlockOrder().empty()) return stats; + const auto order = ReversePostOrder(*program.BlockOrder().front()); + std::unordered_map index; + const auto children = DominatorChildren(order, index); + for (const auto* block : program.BlockOrder()) { + if (!index.contains(block)) throw std::runtime_error("redundant value elimination: block " + std::to_string(block->Id()) + " is unreachable from the entry block"); + } + std::uint64_t nextClobberEpoch = order.size() + 2u; + std::vector firstClobber(order.size(), 0u); + std::vector lastClobber(order.size(), 0u); + for (std::size_t position = 0; position < order.size(); ++position) { + firstClobber[position] = nextClobberEpoch; + for (const auto* inst : order[position]->Instructions()) { + if (ClobbersReadConst(inst->Opcode())) lastClobber[position] = nextClobberEpoch++; + } + } + std::vector entryEpoch(order.size(), 0u); + const auto exitEpoch = [&](std::size_t position) -> std::uint64_t { + if (entryEpoch[position] == 0u) return 0u; + return lastClobber[position] != 0u ? lastClobber[position] : entryEpoch[position]; + }; + for (bool changed = true; changed;) { + changed = false; + for (std::size_t position = 0; position < order.size(); ++position) { + std::uint64_t candidate = position == 0u ? 1u : 0u; + for (const IrBlock* predecessor : order[position]->Predecessors()) { + const auto found = index.find(predecessor); + if (found == index.end()) continue; + const auto incoming = exitEpoch(found->second); + if (incoming == 0u) continue; + if (candidate == 0u) { + candidate = incoming; + } else if (candidate != incoming) { + candidate = position + 2u; + } + } + if (candidate != entryEpoch[position]) { + entryEpoch[position] = candidate; + changed = true; + } + } + } std::unordered_map available; std::vector added; std::unordered_map replacements; - const auto visit = [&](IrBlock& block) { - for (auto* inst : block.Instructions()) { + const auto visit = [&](std::size_t position) { + std::uint64_t epoch = entryEpoch[position]; + std::uint64_t nextClobber = firstClobber[position]; + for (auto* inst : order[position]->Instructions()) { + if (ClobbersReadConst(inst->Opcode())) { + epoch = nextClobber++; + continue; + } if (!Recomputable(inst->Opcode()) || inst->HasImmediate() || !inst->HasUses()) continue; - ValueKey key{inst->Opcode(), inst->Type(), inst->Flags()}; + ValueKey key{inst->Opcode(), inst->Type(), inst->Flags(), inst->Opcode() == IrOpcode::ReadConst ? epoch : 0u}; key.operandCount = inst->ArgumentCount(); if (key.operandCount > key.operands.size()) continue; for (std::size_t index = 0; index < key.operandCount; ++index) key.operands[index] = inst->Argument(index)->Resolve(); @@ -242,22 +300,15 @@ RedundantValueEliminationStats RedundantValueEliminator::Eliminate(IrProgram& pr ++stats.replacedValues; } }; - if (program.BlockOrder().empty()) return stats; - const auto order = ReversePostOrder(*program.BlockOrder().front()); - std::unordered_map index; - const auto children = DominatorChildren(order, index); - for (const auto* block : program.BlockOrder()) { - if (!index.contains(block)) throw std::runtime_error("redundant value elimination: block " + std::to_string(block->Id()) + " is unreachable from the entry block"); - } std::vector> stack{{0u, 0u}}; std::vector scopes{added.size()}; - visit(*order[0]); + visit(0u); while (!stack.empty()) { auto& [position, next] = stack.back(); if (next < children[position].size()) { const auto child = index.at(children[position][next++]); scopes.push_back(added.size()); - visit(*order[child]); + visit(child); stack.emplace_back(child, 0u); continue; } From f2a0dc0b746b825159fc10ddf58592b65e099e0e Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:01:19 +0300 Subject: [PATCH 154/318] test(shader): cover GetSrtResource and ReadConst deduplication in RVE --- .../tests/RedundantValueElimination.cpp | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp index a17b7cf87d..cc88c42cfe 100644 --- a/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/RedundantValueElimination.cpp @@ -324,6 +324,149 @@ int main() { return b.eliminate() == 1u && !dominated.HasUses() && first.UseCount() == 2u && AllUsed({&thenOnly, &sibling, &merged}); }); + passed &= run("GetSrtResource and ReadConst with the same operands merge in a dominated block, while another slot or a sibling block stays", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& then = b.newBlock(); + IrBlock& otherwise = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&then); + head.AddBranch(&otherwise); + then.AddBranch(&merge); + otherwise.AddBranch(&merge); + auto& srtHead = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {&b.constant(0u)}); + auto& readHead = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtHead, &b.constant(2u)}); + b.keep(readHead); + b.block = &then; + auto& srtDominated = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {&b.constant(0u)}); + auto& readDominated = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtDominated, &b.constant(2u)}); + auto& readOtherSlot = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtDominated, &b.constant(3u)}); + auto& srtThenOnly = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {&b.constant(1u)}); + auto& readThenOnly = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtThenOnly, &b.constant(4u)}); + b.keep(readDominated); + b.keep(readOtherSlot); + b.keep(readThenOnly); + b.block = &otherwise; + auto& srtSibling = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {&b.constant(1u)}); + auto& readSibling = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtSibling, &b.constant(4u)}); + b.keep(readSibling); + b.block = &merge; + auto& srtMerge = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {&b.constant(0u)}); + auto& readMerge = b.emit(IrOpcode::ReadConst, IrType::U32, {&srtMerge, &b.constant(2u)}); + b.keep(readMerge); + return b.eliminate() == 4u && !srtDominated.HasUses() && !readDominated.HasUses() && !srtMerge.HasUses() && !readMerge.HasUses() && srtHead.UseCount() == 4u && readHead.UseCount() == 3u && AllUsed({&readOtherSlot, &srtThenOnly, &readThenOnly, &srtSibling, &readSibling}); + }); + + passed &= run("two ReadConst instructions of the same slot separated by a store to the table address stay", [] { + Builder b; + auto& tableAddress = b.emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&b.constant(0x1000u), &b.constant(0u)}); + auto& tableRead = b.emit(IrOpcode::LoadAddressU32, IrType::U32, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.truth()}); + b.program.Resources().srtReads.push_back({&tableRead, 0u}); + auto& srt = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); + auto& first = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + static_cast(b.emit(IrOpcode::StoreAddressU32, IrType::Void, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.constant(7u), &b.truth()})); + auto& second = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(first); + b.keep(second); + return b.eliminate() == 0u && AllUsed({&first, &second}); + }); + + passed &= run("a store on one branch keeps ReadConst in the merge block while the clean sibling branch reuses the head read", [] { + Builder b; + IrBlock& head = *b.block; + IrBlock& then = b.newBlock(); + IrBlock& otherwise = b.newBlock(); + IrBlock& merge = b.newBlock(); + head.AddBranch(&then); + head.AddBranch(&otherwise); + then.AddBranch(&merge); + otherwise.AddBranch(&merge); + auto& tableAddress = b.emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&b.constant(0x1000u), &b.constant(0u)}); + auto& tableRead = b.emit(IrOpcode::LoadAddressU32, IrType::U32, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.truth()}); + b.program.Resources().srtReads.push_back({&tableRead, 0u}); + auto& srt = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); + auto& readHead = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(readHead); + b.block = &then; + static_cast(b.emit(IrOpcode::StoreAddressU32, IrType::Void, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.constant(7u), &b.truth()})); + auto& readThenAfterStore = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(readThenAfterStore); + b.block = &otherwise; + auto& readOtherwise = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(readOtherwise); + b.block = &merge; + auto& readMerge = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(readMerge); + return b.eliminate() == 1u && !readOtherwise.HasUses() && readHead.UseCount() == 2u && AllUsed({&readHead, &readThenAfterStore, &readMerge}); + }); + + passed &= run("a store on a loop backedge prevents reusing a pre-loop ReadConst in the loop header", [] { + Builder b; + IrBlock& header = b.newBlock(); + IrBlock& body = b.newBlock(); + IrBlock& exit = b.newBlock(); + b.block->AddBranch(&header); + header.AddBranch(&body); + header.AddBranch(&exit); + body.AddBranch(&header); + auto& tableAddress = b.emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&b.constant(0x1000u), &b.constant(0u)}); + auto& tableRead = b.emit(IrOpcode::LoadAddressU32, IrType::U32, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.truth()}); + b.program.Resources().srtReads.push_back({&tableRead, 0u}); + auto& srt = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); + auto& beforeLoop = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(beforeLoop); + b.block = &header; + auto& inHeader = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inHeader); + b.block = &body; + auto& inBodyBeforeStore = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + static_cast(b.emit(IrOpcode::StoreAddressU32, IrType::Void, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.constant(7u), &b.truth()})); + auto& inBodyAfterStore = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inBodyBeforeStore); + b.keep(inBodyAfterStore); + b.block = &exit; + auto& afterLoop = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(afterLoop); + return b.eliminate() == 2u && !inBodyBeforeStore.HasUses() && !afterLoop.HasUses() && beforeLoop.UseCount() == 1u && inHeader.UseCount() == 3u && AllUsed({&beforeLoop, &inHeader, &inBodyAfterStore}); + }); + + passed &= run("a store in an inner loop propagates to the outer loop header across fixed-point iterations", [] { + Builder b; + IrBlock& outerHeader = b.newBlock(); + IrBlock& innerHeader = b.newBlock(); + IrBlock& innerBody = b.newBlock(); + IrBlock& outerLatch = b.newBlock(); + IrBlock& exit = b.newBlock(); + b.block->AddBranch(&outerHeader); + outerHeader.AddBranch(&innerHeader); + outerHeader.AddBranch(&exit); + innerHeader.AddBranch(&innerBody); + innerHeader.AddBranch(&outerLatch); + innerBody.AddBranch(&innerHeader); + outerLatch.AddBranch(&outerHeader); + auto& tableAddress = b.emit(IrOpcode::GetAddressResource, IrType::AddressResource, {&b.constant(0x1000u), &b.constant(0u)}); + auto& tableRead = b.emit(IrOpcode::LoadAddressU32, IrType::U32, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.truth()}); + b.program.Resources().srtReads.push_back({&tableRead, 0u}); + auto& srt = b.emit(IrOpcode::GetSrtResource, IrType::SrtResource, {}); + auto& beforeOuter = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(beforeOuter); + b.block = &outerHeader; + auto& inOuterHeader = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inOuterHeader); + b.block = &innerHeader; + auto& inInnerHeader = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inInnerHeader); + b.block = &innerBody; + static_cast(b.emit(IrOpcode::StoreAddressU32, IrType::Void, {&tableAddress, &b.constant(8u), &b.constant(0u), &b.constant(7u), &b.truth()})); + b.block = &outerLatch; + auto& inOuterLatch = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inOuterLatch); + b.block = &exit; + auto& inExit = b.emit(IrOpcode::ReadConst, IrType::U32, {&srt, &b.constant(0u)}); + b.keep(inExit); + return b.eliminate() == 2u && !inOuterLatch.HasUses() && !inExit.HasUses() && beforeOuter.UseCount() == 1u && inOuterHeader.UseCount() == 2u && inInnerHeader.UseCount() == 2u && AllUsed({&beforeOuter, &inOuterHeader, &inInnerHeader}); + }); + passed &= run("a loop body reuses a value of its header, the loop exit does not reuse a value of the body", [] { Builder b; IrBlock& header = b.newBlock(); From 375b53d91851689681101f951ce4ee7423d09326 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 21 Oct 2026 22:30:43 +0100 Subject: [PATCH 155/318] refactor(libkernel): move pselect into libkernel The PS5 3.20 export lists have pselect in libkernel as well as libkernel_sys, and on Windows a libkernel import never reaches libkernel_sys. The implementation moves unchanged to libkernel/Socket/src/Pselect.cpp, beside select. --- core/libs/prx/libkernel/CMakeLists.txt | 1 + .../libs/prx/libkernel/Socket/src/Pselect.cpp | 33 +++++++++++++++++++ .../prx/libkernel_sys/Export.Discovery.cpp | 27 --------------- docs/dev/debt/libkernel.md | 1 + docs/dev/debt/libkernel_sys.md | 1 - 5 files changed, 35 insertions(+), 28 deletions(-) create mode 100644 core/libs/prx/libkernel/Socket/src/Pselect.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 8613c79da0..338210fb77 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -46,6 +46,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Tsd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Sem.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Pthread/Posix/Spinlock.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Pselect.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Socket.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/Udp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Socket/src/SocketOptions.cpp diff --git a/core/libs/prx/libkernel/Socket/src/Pselect.cpp b/core/libs/prx/libkernel/Socket/src/Pselect.cpp new file mode 100644 index 0000000000..931cc83a54 --- /dev/null +++ b/core/libs/prx/libkernel/Socket/src/Pselect.cpp @@ -0,0 +1,33 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include + +extern "C" { + +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); +int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); + +int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask) { + constexpr int SetMask = 3; + struct Timeval { + std::int64_t seconds; + std::int64_t microseconds; + } limit{}; + const Timeval* wait = nullptr; + if (timeout != nullptr) { + const auto* spec = static_cast(timeout); + limit = {spec[0], spec[1] / 1000}; + wait = &limit; + } + std::uint32_t previous[4]{}; + if (sigmask != nullptr && sigprocmask_nid_postfix(SetMask, sigmask, previous) != 0) return -1; + const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); + if (sigmask != nullptr) { + const int error = *__error_nid_postfix(); + sigprocmask_nid_postfix(SetMask, previous, nullptr); + *__error_nid_postfix() = error; + } + return result; +} + +} diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index dcf727762f..2ebe4db765 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -353,33 +353,6 @@ int APS5_VABI profil_nid_postfix(void) { return 0; } -int* APS5_VABI __error_nid_postfix(); -int APS5_VABI select_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout); -int APS5_VABI sigprocmask_nid_postfix(int how, const void* set, void* previousSet); - -int APS5_VABI pselect_nid_postfix(int nfds, void* readfds, void* writefds, void* exceptfds, const void* timeout, const void* sigmask) { - constexpr int SetMask = 3; - struct Timeval { - std::int64_t seconds; - std::int64_t microseconds; - } limit{}; - const Timeval* wait = nullptr; - if (timeout != nullptr) { - const auto* spec = static_cast(timeout); - limit = {spec[0], spec[1] / 1000}; - wait = &limit; - } - std::uint32_t previous[4]{}; - if (sigmask != nullptr && sigprocmask_nid_postfix(SetMask, sigmask, previous) != 0) return -1; - const int result = select_nid_postfix(nfds, readfds, writefds, exceptfds, wait); - if (sigmask != nullptr) { - const int error = *__error_nid_postfix(); - sigprocmask_nid_postfix(SetMask, previous, nullptr); - *__error_nid_postfix() = error; - } - return result; -} - int APS5_VABI pthread_attr_getaffinity_np_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 381e509889..65c312cd2b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -147,6 +147,7 @@ - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit - [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store - [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the current user's `ROOT` store, filtered through Disallowed and the combined EKU extension/property for TLS server authentication (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/ssl/ca-bundle.pem` and `/etc/ssl/cert.pem` that exists. On macOS, `/etc/ssl/cert.pem` is a static snapshot and does not reflect roots added to or distrusted in the Keychain. Existing guest files, explicit aliases and blocked mounts take precedence over the provider. The PEM snapshot does not carry Windows CTL restrictions beyond EKU, native revocation policy or later trust-store changes. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified +- [pselect](../../../core/libs/prx/libkernel/Socket/src/Pselect.cpp) (libkernel) sets the signal mask with `sigprocmask`, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does, but the swap is not atomic with the wait: a signal that becomes deliverable between the swap and the wait is not guaranteed to interrupt it - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index ff16c7e9af..25499ae3d1 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -14,7 +14,6 @@ - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fstatat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `stat`, or to `lstat` with `AT_SYMLINK_NOFOLLOW` (0x200). Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory, and flags other than `AT_SYMLINK_NOFOLLOW` return `EINVAL`. PPSA74126 imports it. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified -- [pselect](../../../core/libs/prx/libkernel_sys/Export.Discovery.cpp) (libkernel_sys) sets the signal mask with `sigprocmask`, calls `select` and restores the mask, as FreeBSD 11 `kern_pselect` does, but the swap is not atomic with the wait: a signal that becomes deliverable between the swap and the wait is not guaranteed to interrupt it - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. Console behaviour is unverified - [fork](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws: creating a child process is not supported. PPSA19111 imports it and fails to load without it. Its engine calls `fork` in `Posix_Daemonize`, only with `-daemonize`, and in `Platform_ShellExecute` before `execvp("xdg-open")`, which the main menu's Previews button reaches, so pressing Previews ends in this throw. [setsid](../../../core/libs/prx/libkernel_sys/Export.cpp) fails with `EPERM`, as FreeBSD's does for a process group leader, since the guest is taken to lead its own process group (see `kill`). [umask](../../../core/libs/prx/libkernel_sys/Export.cpp) keeps the guest's mask (initially 022, masked to 07777) and returns the previous one, Linux named shared-memory creation applies it, but ordinary files are still created with the host process's mask - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) From f066bd61b3da25c3a7fef7d6d01f6be9bff0c3d3 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 14 Oct 2026 04:39:33 -0400 Subject: [PATCH 156/318] feat(audio-propagation): track source-owned configurable path slots --- .../AudioPropagation.hpp | 10 ++ .../prx/libSceAudioPropagation/Export.cpp | 91 +++++++++++++++---- core/libs/tests/GuestAudioPropagation.cpp | 70 ++++++++++++-- docs/dev/debt/libSceAudioPropagation.md | 4 +- 4 files changed, 149 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp index 8323b813c9..f00b2ea48a 100644 --- a/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp +++ b/core/libs/prx/libSceAudioPropagation/AudioPropagation.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAUDIOPROPAGATION_AUDIOPROPAGATION_HPP #define CORE_LIBS_PRX_LIBSCEAUDIOPROPAGATION_AUDIOPROPAGATION_HPP +#include #include #include @@ -128,6 +129,14 @@ static_assert(offsetof(ReportInfo, function) == 32); static_assert(offsetof(ReportInfo, format) == 40); static_assert(offsetof(ReportInfo, arguments) == 48); +inline constexpr std::uint32_t MaxPaths = 36; +inline constexpr std::uint32_t MaxPathPoints = 4; +inline constexpr std::uint32_t PathId = 0x010107d3; +inline constexpr std::uint32_t PathPointId = 0x010107d2; +inline constexpr std::uint32_t PathInvalid = 1; +inline constexpr std::uint32_t PathPortal = 2; + + struct Attribute { std::uint32_t id; std::uint32_t reserved0; @@ -209,6 +218,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceDestroy(AudioPropagationHandle s std::int32_t APS5_VABI sceAudioPropagationSourceSetAttributes(AudioPropagationHandle source, const AudioPropagation::Attribute* attributes, std::uint32_t count); std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagationHandle source, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHandle source, std::uint32_t index, AudioPropagationHandle* outPath); +std::int32_t APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count); std::int32_t APS5_VABI sceAudioPropagationSourceCalculateAudioPaths(AudioPropagationHandle source, const void* rays, std::uint32_t rayCount, std::uint32_t reflectionOrder, void* paths, std::uint32_t pathCount); std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHandle source, const void* entries, std::uint32_t count); diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index fb835aca4d..3458ff2530 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -1,8 +1,8 @@ #include "AudioPropagation.hpp" #include "prx/libc/include/General.hpp" -#include #include +#include #include #include #include @@ -27,18 +27,25 @@ struct SystemState { std::uint32_t maxSources; }; -enum class Kind { Room, Portal, Source, Material }; +enum class Kind { Room, Portal, Source, Material, Path }; struct Object { Kind kind; AudioPropagationHandle system; }; +struct PathState { + AudioPath data{}; + float gain = 0.0f; +}; + struct Registry { std::mutex mutex; std::unordered_set systems; std::unordered_map objects; std::unordered_map orphans; + std::unordered_map> sourcePaths; + std::unordered_map paths; AudioPropagationHandle next = 1; }; @@ -101,6 +108,13 @@ void destroyObject(AudioPropagationHandle system, AudioPropagationHandle handle, return; } require(requireObject(state, handle, kind, function).system == system, function); + if (kind == Kind::Source) { + for (const auto path : state.sourcePaths.at(handle)) { + state.paths.erase(path); + state.objects.erase(path); + } + state.sourcePaths.erase(handle); + } state.objects.erase(handle); } @@ -169,9 +183,11 @@ std::int32_t APS5_VABI sceAudioPropagationSystemDestroy(AudioPropagationHandle s auto& state = registry(); std::lock_guard lock(state.mutex); requireSystem(state, system, __func__); + std::erase_if(state.paths, [&](const auto& entry) { return state.objects.at(entry.first).system == system; }); + std::erase_if(state.sourcePaths, [&](const auto& entry) { return state.objects.at(entry.first).system == system; }); std::erase_if(state.objects, [&](const auto& entry) { if (entry.second.system != system) return false; - state.orphans.insert(entry); + if (entry.second.kind != Kind::Path) state.orphans.insert(entry); return true; }); reinterpret_cast(system)->magic = 0; @@ -257,7 +273,30 @@ std::int32_t APS5_VABI sceAudioPropagationPortalSetAttributes(AudioPropagationHa } std::int32_t APS5_VABI sceAudioPropagationSourceCreate(AudioPropagationHandle system, AudioPropagationHandle* outSource) { - createObject(system, Kind::Source, outSource, __func__); + require(outSource != nullptr, __func__); + auto& state = registry(); + std::lock_guard lock(state.mutex); + AudioPropagationHandle source = 0; + createObjectLocked(state, system, Kind::Source, &source, __func__); + std::array handles{}; + try { + for (auto& handle : handles) { + createObjectLocked(state, system, Kind::Path, &handle, __func__); + PathState path{}; + path.data.desc = {PathId, sizeof(AudioPath)}; + path.data.flags = PathInvalid; + state.paths.emplace(handle, path); + } + state.sourcePaths.emplace(source, handles); + } catch (...) { + for (const auto handle : handles) { + state.paths.erase(handle); + state.objects.erase(handle); + } + state.objects.erase(source); + throw; + } + *outSource = source; return 0; } @@ -279,7 +318,7 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPathCount(AudioPropagati std::lock_guard lock(state.mutex); requireObject(state, source, Kind::Source, __func__); require(count != nullptr, __func__); - *count = 0; + *count = MaxPaths; return 0; } @@ -289,8 +328,18 @@ std::int32_t APS5_VABI sceAudioPropagationSourceGetAudioPath(AudioPropagationHan const auto found = state.objects.find(source); if (found == state.objects.end() || found->second.kind != Kind::Source || !liveSystem(state, found->second.system)) return ErrorInvalidHandle; if (!outPath) return ErrorNullPointer; - static_cast(index); - return ErrorInvalidArgument; + if (index >= MaxPaths) return ErrorInvalidArgument; + *outPath = state.sourcePaths.at(source)[index]; + return 0; +} + +std::int32_t APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count) { + auto& state = registry(); + std::lock_guard lock(state.mutex); + requireObject(state, path, Kind::Path, __func__); + require(count != nullptr, __func__); + *count = state.paths.at(path).data.pointCount; + return 0; } std::int32_t APS5_VABI sceAudioPropagationSourceGetRays(AudioPropagationHandle source, void* rays, std::uint32_t* count) { @@ -322,10 +371,19 @@ std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPaths(AudioPropagationHa } std::int32_t APS5_VABI sceAudioPropagationSourceSetAudioPath(AudioPropagationHandle path, const AudioPath* data, float gain) { - static_cast(path); - static_cast(data); - static_cast(gain); - NotImplemented_nid_no_patch(__func__); + auto& state = registry(); + std::lock_guard lock(state.mutex); + const auto system = requireObject(state, path, Kind::Path, __func__).system; + requireDescriptor(data != nullptr ? &static_cast(data)->desc : nullptr, PathId, sizeof(AudioPath), __func__); + const auto replacement = *static_cast(data); + require(std::isfinite(gain) && gain >= 0.0f && replacement.pointCount <= MaxPathPoints && (replacement.flags & ~(PathInvalid | PathPortal)) == 0, __func__); + for (std::uint32_t index = 0; index < replacement.pointCount; ++index) { + const auto& point = replacement.points[index]; + requireDescriptor(&point.desc, PathPointId, sizeof(PathPoint), __func__); + require(std::isfinite(point.gain) && point.gain >= 0.0f && std::isfinite(point.position[0]) && std::isfinite(point.position[1]) && std::isfinite(point.position[2]), __func__); + if (point.room != 0) require(requireObject(state, point.room, Kind::Room, __func__).system == system, __func__); + } + state.paths.at(path) = PathState{replacement, gain}; return 0; } @@ -343,6 +401,12 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy require(requireObject(state, info.source, Kind::Source, __func__).system == system, __func__); require(info.output != nullptr && info.outputSize >= requiredBytes, __func__); if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); + for (const auto handle : state.sourcePaths.at(info.source)) { + const auto& path = state.paths.at(handle); + if (path.gain > 0.0f && path.data.pointCount != 0 && (path.data.flags & PathInvalid) == 0) { + throw std::runtime_error(std::string(__func__) + ": nonempty audio path rendering is not implemented"); + } + } } for (std::uint32_t index = 0; index < count; ++index) { infos[index].channelCount = 0; @@ -350,11 +414,6 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy return 0; } -int APS5_VABI sceAudioPropagationPathGetNumPoints(AudioPropagationHandle path, std::uint32_t* count) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAudioPropagationReportApi(std::uint32_t category, AudioPropagationHandle handle, std::int32_t result, std::int32_t operation, const char* function, const char* format, ...) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index 0f060fd0a5..ebf9b6874a 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -5,10 +5,10 @@ #include #include #include +#include #include extern "C" { -int APS5_VABI sceAudioPropagationPathGetNumPoints(); int APS5_VABI sceAudioPropagationSystemLock(); int APS5_VABI sceAudioPropagationMaterialInit(); } @@ -152,13 +152,48 @@ int main() { RequireThrows([&] { sceAudioPropagationSourceCreate(system, nullptr); }, "source without output"); Require(sceAudioPropagationSourceSetAttributes(source, portalAttributes, 3) == 0, "source attributes failed"); std::uint32_t count = 7; - Require(sceAudioPropagationSourceGetAudioPathCount(source, &count) == 0 && count == 0, "source reports audio paths"); + Require(sceAudioPropagationSourceGetAudioPathCount(source, &count) == 0 && count == AudioPropagation::MaxPaths, "source path pool size"); AudioPropagationHandle path = 0; - path = 0x1234; - Require(sceAudioPropagationSourceGetAudioPath(source, 0, &path) == AudioPropagation::ErrorInvalidArgument && path == 0x1234, "audio path without paths"); - Require(sceAudioPropagationSourceGetAudioPath(source, UINT32_MAX, &path) == AudioPropagation::ErrorInvalidArgument && path == 0x1234, "audio path index range"); - Require(sceAudioPropagationSourceGetAudioPath(source, 0, nullptr) == AudioPropagation::ErrorNullPointer, "audio path null output"); - Require(sceAudioPropagationSourceGetAudioPath(0, 0, nullptr) == AudioPropagation::ErrorInvalidHandle, "audio path handle precedence"); + Require(sceAudioPropagationSourceGetAudioPath(source, 0, &path) == 0 && path != 0, "path lookup immediately after source creation"); + AudioPropagationHandle repeated = 0; + Require(sceAudioPropagationSourceGetAudioPath(source, 0, &repeated) == 0 && repeated == path, "path handle is unstable"); + AudioPropagationHandle lastPath = 0; + Require(sceAudioPropagationSourceGetAudioPath(source, AudioPropagation::MaxPaths - 1, &lastPath) == 0 && lastPath != path, "last path slot"); + Require(sceAudioPropagationSourceGetAudioPath(source, AudioPropagation::MaxPaths, &repeated) == AudioPropagation::ErrorInvalidArgument, "out of range path slot"); + Require(repeated == path, "rejected path lookup changed the output"); + Require(sceAudioPropagationSourceGetAudioPath(source, 0, nullptr) == AudioPropagation::ErrorNullPointer, "null path output"); + Require(sceAudioPropagationSourceGetAudioPath(0, 0, nullptr) == AudioPropagation::ErrorInvalidHandle, "path lookup handle precedence"); + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 0, "initial path has points"); + RequireThrows([&] { sceAudioPropagationPathGetNumPoints(source, &count); }, "source used as a path"); + RequireThrows([&] { sceAudioPropagationPathGetNumPoints(path, nullptr); }, "null point-count output"); + AudioPropagation::AudioPath configured{}; + configured.desc = {AudioPropagation::PathId, sizeof(configured)}; + configured.pointCount = 2; + configured.flags = AudioPropagation::PathInvalid; + for (auto& point : configured.points) { + point.desc = {AudioPropagation::PathPointId, sizeof(point)}; + point.gain = 1.0f; + } + configured.points[0].position[0] = 3.0f; + Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "path configuration"); + configured.pointCount = 0; + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 2, "path configuration aliases caller memory"); + configured.pointCount = 2; + auto badPath = configured; + badPath.desc.size--; + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "wrong path size"); + badPath = configured; + badPath.pointCount = AudioPropagation::MaxPathPoints + 1; + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "too many path points"); + badPath = configured; + badPath.points[0].room = materialHandle; + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "material used as a path room"); + badPath = configured; + badPath.points[0].position[0] = std::numeric_limits::infinity(); + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "nonfinite point position"); + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &configured, std::numeric_limits::quiet_NaN()); }, "nonfinite path gain"); + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, nullptr, 1.0f); }, "null path data"); + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 2, "rejected update changed path state"); std::uint8_t rays[AudioPropagation::RaySize * 2]{}; count = 7; @@ -186,6 +221,13 @@ int main() { Require(sceAudioPropagationSystemCreate(&options, &otherMemory, &otherSystem) == 0, "second system create failed"); AudioPropagationHandle otherSource = 0; Require(sceAudioPropagationSourceCreate(otherSystem, &otherSource) == 0, "second system source create failed"); + AudioPropagationHandle foreignRoom = 0; + Require(sceAudioPropagationRoomCreate(otherSystem, &foreignRoom) == 0, "foreign room create failed"); + badPath = configured; + badPath.points[0].room = foreignRoom; + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "room from another system"); + Require(sceAudioPropagationRoomDestroy(otherSystem, foreignRoom) == 0, "foreign room cleanup"); + Require(sceAudioPropagationPathGetNumPoints(path, &count) == 0 && count == 2, "cross-system rejection changed path state"); AudioPropagationHandle secondSource = 0; Require(sceAudioPropagationSourceCreate(system, &secondSource) == 0, "second source create failed"); @@ -265,6 +307,14 @@ int main() { RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render with an unknown format"); Require(untouched(), "render with an unknown format wrote an output"); + reset(); + configured.flags = 0; + Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "active path configuration"); + RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "active path silently rendered"); + Require(untouched(), "unsupported active-path render modified output"); + configured.flags = AudioPropagation::PathInvalid; + Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "invalidating path"); + Require(sceAudioPropagationSourceDestroy(system, secondSource) == 0, "second source destroy failed"); reset(); RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render of a destroyed source"); @@ -274,13 +324,18 @@ int main() { RequireThrows([&] { sceAudioPropagationSourceDestroy(system + 16, source); }, "source destroy with another system"); Require(sceAudioPropagationPortalDestroy(system, portal) == 0, "portal destroy failed"); Require(sceAudioPropagationSourceDestroy(system, source) == 0, "source destroy failed"); + RequireThrows([&] { sceAudioPropagationPathGetNumPoints(path, &count); }, "path survived source destruction"); + RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(lastPath, &configured, 1.0f); }, "path update after source destruction"); Require(sceAudioPropagationRoomDestroy(system, rooms[0]) == 0, "room destroy failed"); AudioPropagationHandle lateSource = 0; AudioPropagationHandle lateMaterial = 0; Require(sceAudioPropagationSourceCreate(system, &lateSource) == 0, "late source create failed"); + AudioPropagationHandle latePath = 0; + Require(sceAudioPropagationSourceGetAudioPath(lateSource, 0, &latePath) == 0 && latePath != path, "path handle reuse"); Require(sceAudioPropagationSystemRegisterMaterial(system, material, &lateMaterial) == 0, "late material register failed"); Require(sceAudioPropagationSystemDestroy(system) == 0, "system destroy failed"); + RequireThrows([&] { sceAudioPropagationPathGetNumPoints(latePath, &count); }, "path survived system destruction"); RequireThrows([&] { sceAudioPropagationSourceSetAttributes(lateSource, &attribute, 1); }, "source used after its system"); RequireThrows([&] { sceAudioPropagationRoomDestroy(system, lateSource); }, "orphan destroyed as the wrong kind"); RequireThrows([&] { sceAudioPropagationSourceDestroy(system + 16, lateSource); }, "orphan destroyed with another system"); @@ -292,7 +347,6 @@ int main() { Require(sceAudioPropagationSystemCreate(&options, &memory, &system) == 0, "memory reuse after destroy failed"); Require(sceAudioPropagationSystemDestroy(system) == 0, "second destroy failed"); - RequireThrows([&] { sceAudioPropagationPathGetNumPoints(); }, "path get num points throw"); RequireThrows([&] { sceAudioPropagationSystemLock(); }, "system lock throw"); RequireThrows([&] { sceAudioPropagationMaterialInit(); }, "material init throw"); diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index cff417667f..33f0ca3d13 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -2,10 +2,10 @@ ### Silent stubs -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) lacks acoustic propagation: attributes have no effect, sources expose no paths or rays, rendering preserves the destination mix without adding path contributions, and allocated CPU work memory is mostly unused. Ray results and audio paths accept empty lists and throw otherwise; calculation returns initial native validation errors and throws for unsupported propagation. Room, portal, source and material handles are tracked, but destruction after their system is destroyed silently succeeds without native lifetime semantics being confirmed. Occlusion, reflections and reverb are absent. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) lacks acoustic propagation: attributes have no effect, sources expose 36 initially empty, invalid path slots and no rays, rendering preserves the destination mix without adding path contributions, and allocated CPU work memory is mostly unused. Ray results and audio paths accept empty lists and throw otherwise; calculation returns initial native validation errors and throws for unsupported propagation. Room, portal, source and material handles are tracked, but destruction after their system is destroyed silently succeeds without native lifetime semantics being confirmed. Occlusion, reflections and reverb are absent. ### Unknown function info -- [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): the native path lookup and update confirm a 64-bit path handle, a read-only 224-byte path description and a float gain in XMM0. The description contains four 48-byte points, a point count and flags; each point contains a descriptor, room handle, gain and three position floats. The last eight bytes of each point and of the description are not consumed by these paths and remain opaque. Protected implementation remains unchanged; allocation, ownership and rendering are not implemented. +- [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): native evidence establishes a 64-bit path handle, a read-only 224-byte path description and a float gain. The four 48-byte points contain a room handle, gain and position; trailing eight-byte fields remain opaque. Updates copy the description and validate room ownership. Each source owns 36 initially invalid, empty slots, following public SharpProspero MaxPaths and SAROS's immediate lookup of slot 0. Allocation before calculation, the count of 36 and update validation precedence are inferred, not measured on a console. Native lookup error codes and their handle/null/index precedence are retained. Source/system destruction invalidates owned slots. Active nonempty path rendering still throws. - [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes one silent frame without native accumulation. Frame capacity and source-count limits follow the recovered options. Other formats and native validation precedence remain unimplemented. - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; diagnostic layouts are recovered, but source attributes 2/3 and the constant-one fields still lack semantic names. The final system word and the fourth position/direction words have no confirmed semantics. From 7bfcc9d07aa1a5b57af1659a3c475d4a5f85a689 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 21 Oct 2026 05:29:56 -0400 Subject: [PATCH 157/318] fix(audio-propagation): accept the public render output formats sceAudioPropagationSourceRender accepted only format 2. The RenderInfo word at +0x28 is SharpProspero's AudioPropagationOutputFormat: 0, 1 and 2 are first- to third-order Ambisonics (4, 9 and 16 float channels), 4 and 5 are 8 interleaved float channels. SAROS (PPSA07631) renders format 0. Other values still throw, and an output that is not a whole number of frames is rejected. Based on db91769497 from the JJenkx fork. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAudioPropagation/Export.cpp | 15 +++++++-- core/libs/tests/GuestAudioPropagation.cpp | 32 ++++++++++++++++--- docs/dev/debt/libSceAudioPropagation.md | 2 +- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAudioPropagation/Export.cpp b/core/libs/prx/libSceAudioPropagation/Export.cpp index 3458ff2530..2d5b0afba2 100644 --- a/core/libs/prx/libSceAudioPropagation/Export.cpp +++ b/core/libs/prx/libSceAudioPropagation/Export.cpp @@ -394,13 +394,22 @@ std::int32_t APS5_VABI sceAudioPropagationSourceRender(AudioPropagationHandle sy requireSystem(state, system, __func__); const auto& configuration = *reinterpret_cast(system); require(count <= configuration.maxSources, __func__); - const auto requiredBytes = static_cast(configuration.frameSamples) * 16 * sizeof(float); for (std::uint32_t index = 0; index < count; ++index) { const auto& info = infos[index]; requireDescriptor(&info.desc, RenderInfoId, RenderInfoSize, __func__); require(requireObject(state, info.source, Kind::Source, __func__).system == system, __func__); - require(info.output != nullptr && info.outputSize >= requiredBytes, __func__); - if (info.format != RenderFormat) throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); + require(info.output != nullptr && info.outputSize != 0, __func__); + std::uint32_t channels = 0; + switch (info.format) { + case 0: channels = 4; break; + case 1: channels = 9; break; + case 2: channels = 16; break; + case 4: + case 5: channels = 8; break; + default: throw std::runtime_error(std::string(__func__) + ": render format " + std::to_string(info.format) + " is unknown"); + } + const auto requiredBytes = static_cast(configuration.frameSamples) * channels * sizeof(float); + require(info.outputSize >= requiredBytes && info.outputSize % (channels * sizeof(float)) == 0, __func__); for (const auto handle : state.sourcePaths.at(info.source)) { const auto& path = state.paths.at(handle); if (path.gain > 0.0f && path.data.pointCount != 0 && (path.data.flags & PathInvalid) == 0) { diff --git a/core/libs/tests/GuestAudioPropagation.cpp b/core/libs/tests/GuestAudioPropagation.cpp index ebf9b6874a..b98463d30c 100644 --- a/core/libs/tests/GuestAudioPropagation.cpp +++ b/core/libs/tests/GuestAudioPropagation.cpp @@ -174,6 +174,9 @@ int main() { point.desc = {AudioPropagation::PathPointId, sizeof(point)}; point.gain = 1.0f; } + configured.points[0].room = rooms[0]; + configured.points[0].opaque[0] = std::byte{0x7f}; + configured.opaque[0] = std::byte{0x55}; configured.points[0].position[0] = 3.0f; Require(sceAudioPropagationSourceSetAudioPath(path, &configured, 1.0f) == 0, "path configuration"); configured.pointCount = 0; @@ -186,8 +189,11 @@ int main() { badPath.pointCount = AudioPropagation::MaxPathPoints + 1; RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "too many path points"); badPath = configured; - badPath.points[0].room = materialHandle; + AudioPropagationHandle pathMaterial = 0; + Require(sceAudioPropagationSystemRegisterMaterial(system, material, &pathMaterial) == 0, "path material creation"); + badPath.points[0].room = pathMaterial; RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "material used as a path room"); + Require(sceAudioPropagationSystemUnregisterMaterial(pathMaterial) == 0, "path material cleanup"); badPath = configured; badPath.points[0].position[0] = std::numeric_limits::infinity(); RequireThrows([&] { sceAudioPropagationSourceSetAudioPath(path, &badPath, 1.0f); }, "nonfinite point position"); @@ -302,10 +308,28 @@ int main() { reset(); infos[0].outputSize = 0; RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 1); }, "render with an empty output"); + for (const std::uint32_t format : {0u, 1u, 2u, 4u, 5u}) { + reset(); + infos[0].format = format; + const std::size_t channels = format == 0 ? 4 : format == 1 ? 9 : format == 2 ? 16 : 8; + infos[0].outputSize = sizeof(float) * options.frameSamples * channels; + Require(sceAudioPropagationSourceRender(system, infos, 1) == 0, "render in a known format failed"); + Require(untouched(), "empty source modified a mix in a known format"); + infos[0].outputSize -= sizeof(float) * channels; + RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 1); }, "known format accepted an undersized frame"); + Require(untouched(), "undersized known-format output modified a mix"); + } reset(); - infos[1].format = 1; - RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render with an unknown format"); - Require(untouched(), "render with an unknown format wrote an output"); + infos[0].format = 1; + infos[0].outputSize = sizeof(float) * (options.frameSamples * 9 + 1); + RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 1); }, "render of a partial nine-channel frame"); + Require(untouched(), "render of a partial frame wrote an output"); + for (const std::uint32_t format : {3u, 6u}) { + reset(); + infos[1].format = format; + RequireThrows([&] { sceAudioPropagationSourceRender(system, infos, 2); }, "render with an unknown format"); + Require(untouched(), "render with an unknown format wrote an output"); + } reset(); configured.flags = 0; diff --git a/docs/dev/debt/libSceAudioPropagation.md b/docs/dev/debt/libSceAudioPropagation.md index 33f0ca3d13..31f184e618 100644 --- a/docs/dev/debt/libSceAudioPropagation.md +++ b/docs/dev/debt/libSceAudioPropagation.md @@ -7,5 +7,5 @@ ### Unknown function info - [sceAudioPropagationSourceSetAudioPath](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): native evidence establishes a 64-bit path handle, a read-only 224-byte path description and a float gain. The four 48-byte points contain a room handle, gain and position; trailing eight-byte fields remain opaque. Updates copy the description and validate room ownership. Each source owns 36 initially invalid, empty slots, following public SharpProspero MaxPaths and SAROS's immediate lookup of slot 0. Allocation before calculation, the count of 36 and update validation precedence are inferred, not measured on a console. Native lookup error codes and their handle/null/index precedence are retained. Source/system destruction invalidates owned slots. Active nonempty path rendering still throws. -- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host supports only format 2 and writes one silent frame without native accumulation. Frame capacity and source-count limits follow the recovered options. Other formats and native validation precedence remain unimplemented. +- [libSceAudioPropagation](../../../core/libs/prx/libSceAudioPropagation/Export.cpp): system options use a recovered 56-byte record; the field at byte 24 and final eight bytes remain semantically unresolved. CPU memory requirements and option limits are recovered; GPU memory queries throw because their platform-specific runtime condition is unresolved. Descriptor validation, ray ownership and path generation are unimplemented. Native rendering accepts multiple mutable records; the host accepts public formats 0/1/2/4/5 (4/9/16/8/8 float channels), preserves the output mix without path contributions and clears the returned channel count; non-format-2 behavior remains unverified on a console. Frame capacity and source-count limits follow the recovered options. Other formats throw; native validation precedence remains unimplemented. - [Initialization exports](../../../core/libs/prx/libSceAudioPropagation/Export.cpp) ending in `Init`, plus `sceAudioPropagationSystem`, are absent from the supplied native module and retain unresolved signatures. Query, locking, enumeration and report callback signatures are recovered; diagnostic layouts are recovered, but source attributes 2/3 and the constant-one fields still lack semantic names. The final system word and the fourth position/direction words have no confirmed semantics. From 78bfaba736ea2739d3fe9a32091765ad800b55f5 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:01:33 -0300 Subject: [PATCH 158/318] test(relinker): exercise kernel variant imports --- core/relinker/CMakeLists.txt | 8 ++ .../test_windows_kernel_variant_imports.py | 77 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 core/relinker/relinker/tests/test_windows_kernel_variant_imports.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index fa78fbaa42..6f95673303 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -666,6 +666,14 @@ if(BUILD_TESTING) add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $ ${windows_stdio_host_arguments}) + if(WIN32) + foreach(kernelVariant IN ITEMS sys web) + add_test(NAME windows_kernel_variant_${kernelVariant} COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_kernel_variant_imports.py + $ libkernel_${kernelVariant}.prx) + set_tests_properties(windows_kernel_variant_${kernelVariant} PROPERTIES TIMEOUT 30) + endforeach() + endif() add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $) add_test(NAME weak_imports COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/tests/test_windows_kernel_variant_imports.py b/core/relinker/relinker/tests/test_windows_kernel_variant_imports.py new file mode 100644 index 0000000000..2335566e12 --- /dev/null +++ b/core/relinker/relinker/tests/test_windows_kernel_variant_imports.py @@ -0,0 +1,77 @@ +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +from guest_import_fixtures import executable +from test_windows_import_modules import provider + + +def main(): + assert os.name == 'nt', 'This test executes Windows startup stubs' + relinker = Path(sys.argv[1]).resolve() + variant = sys.argv[2] + assert variant in ('libkernel_sys.prx', 'libkernel_web.prx') + with tempfile.TemporaryDirectory(prefix='anyps5-kernel-variant-') as directory: + work = Path(directory) + modules = work / 'helpers' / 'prx' + modules.mkdir(parents=True) + for name, value in (('variant', 11), ('canonical', 22), ('runtime', 33), ('guest', 44)): + (modules / (name + '.prx')).write_bytes(provider(value)) + empty = provider(0) + empty[0x800:0x80c] = b'\0other#A#B\0\0' + (modules / 'empty.prx').write_bytes(empty) + source = modules.parent / 'input.elf' + source.write_bytes(executable('variant.prx', 'shared#A#B')) + result = subprocess.run([str(relinker), '--windows', str(source), str(modules.parent / 'output.exe')], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, result.stderr + converted = modules.parent / 'app0' / 'prx' + images = {name: (converted / (name + '.prx.guest.prx')).read_bytes() + for name in ('variant', 'canonical', 'runtime', 'guest', 'empty')} + cases = ( + ('runtime-fallback', 'empty', 'empty', 'runtime', 33), + ('variant-first', 'variant', 'canonical', 'runtime', 11), + ('canonical-before-runtime', 'empty', 'canonical', 'runtime', 22), + ('missing', 'empty', 'empty', 'empty', None), + ('guest-isolation', 'empty', 'empty', 'runtime', 33), + ('guest-cannot-fill-missing', 'empty', 'empty', 'empty', None), + ('unrelated-host', 'empty', 'empty', 'empty', None), + ('guest-requested-variant', 'variant', 'canonical', 'runtime', 44), + ('unqualified', 'variant', 'canonical', 'runtime', 11), + ) + for label, requested, canonical, runtime, expected in cases: + case = work / label + libraries = case / 'libs' + libraries.mkdir(parents=True) + (case / 'prx').mkdir() + for name, image in ((variant, requested), ('libkernel.prx', canonical), ('libc.prx', runtime)): + (libraries / name).write_bytes(images[image]) + if label.startswith('guest-'): + guests = case / 'sce_module' + guests.mkdir() + names = (variant,) if label == 'guest-requested-variant' else ('libkernel.prx', 'libc.prx') + for name in names: + (guests / name).write_bytes(provider(44)) + extra = ('unrelated.prx',) if label == 'unrelated-host' else () + if extra: + (libraries / extra[0]).write_bytes(images['guest']) + source = case / 'input.elf' + symbol = 'shared' if label == 'unqualified' else 'shared#A#B' + source.write_bytes(executable(variant, symbol, extra_dependencies=extra)) + output = case / 'output.exe' + result = subprocess.run([str(relinker), '--windows', str(source), str(output)], + capture_output=True, text=True, timeout=30) + assert result.returncode == 0, (variant, label, result.stderr) + result = subprocess.run([str(output)], capture_output=True, text=True, timeout=30) + if expected is None: + assert result.returncode & 0xffffffff == 0xc0000139, (label, result.returncode, result.stderr) + assert 'unresolved ELF import shared' in result.stderr, (label, result.stderr) + else: + assert result.returncode == expected, (variant, label, result.returncode, result.stderr) + print(variant + ': runtime, precedence and isolation checks passed') + + +if __name__ == '__main__': + main() From 962b88a9e1ae43c07742270971ccdee0975fe067 Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 14:18:58 +1300 Subject: [PATCH 159/318] fix(libSceNpCommon): parse sceNpStrToInt32 through a 64-bit integer on every host Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libSceNpCommon/Strings.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/Strings.cpp b/core/libs/prx/libSceNpCommon/Strings.cpp index d5f70019e9..ce17739d6f 100644 --- a/core/libs/prx/libSceNpCommon/Strings.cpp +++ b/core/libs/prx/libSceNpCommon/Strings.cpp @@ -357,11 +357,11 @@ int APS5_VABI sceNpInt64ToStr(char* buf, std::size_t size, std::int64_t value, s } int APS5_VABI sceNpStrToInt32(const char* str, std::int32_t* value, int base, std::size_t* parsedLength) { - long parsed = 0; + long long parsed = 0; bool saturated = false; const int result = ParseInteger(str, parsedLength, [&](const char* s, char** end) { - parsed = std::strtol(s, end, base); - saturated = parsed == LONG_MIN || parsed == LONG_MAX; + parsed = std::strtoll(s, end, base); + saturated = parsed == LLONG_MIN || parsed == LLONG_MAX; }, &saturated); if (result < 0) return result; if (parsed != static_cast(parsed)) return NP_ERROR_OUT_OF_RANGE; From e8abcba78fb6cf9c9f4dd3585ca7d020b34a0774 Mon Sep 17 00:00:00 2001 From: hypermist Date: Thu, 22 Oct 2026 14:43:14 +1300 Subject: [PATCH 160/318] docs(agc): add the console's measured workload packet details Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- docs/dev/debt/libSceAgc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index 01dd1bfcf3..d02567c947 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -44,6 +44,6 @@ - [sceAgcSetAmmSemaphoreMemory](../../../core/libs/prx/libSceAgc/Misc/src/Semaphore.cpp) - native registration first invokes a kernel ioctl and driver registration; host registration does not reproduce their memory-access checks or error propagation. Null, empty and address-wrapping ranges throw. The full range is cleared after successful registration; duplicate registration, alignment and label-range errors match the native wrapper. - [sceAgcDcbSetWorkloadsActive and sceAgcDcbSetWorkloadComplete](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) (libSceAgc) and [sceAgcDriverRegisterWorkloadStream](../../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - the driver call records only the stream id (1 to 31; it returns 0 for any argument and keeps no stream record), and the DCB calls write the packets [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/agc.cpp) writes: an 18-dword (active) or 12-dword (complete) `IT_NOP` packet with the stream id, and for active the 64-bit mask of the workload ids, for complete the workload id and the 64-bit clear mask of that id. Both return null for a null command buffer, an unregistered or out-of-range stream, a workload id above 63, and for active a duplicate id or a count of 0 or above 63. The layouts and checks come from KytyPS5 alone; the console oracle in [prosper](https://github.com/mattias800/prosper/blob/main/prosper/tests/data/console_oracle/agcbuild.golden.tsv) measures the null return only for unregistered streams, not the packet contents of a registered one. The error codes the console driver returns for an invalid or repeated registration are unknown, so registration never fails here. PPSA29866 calls sceAgcDcbSetWorkloadsActive while building its command buffers (sceAgcDcbSetWorkloadComplete was not observed); nothing else was observed. - [sceAgcDcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkloadsActive, sceAgcAcbSetWorkloadComplete, sceAgcAcbSetWorkloadStreamInactive](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - return null without writing to the buffer for a null command buffer or a stream that is not registered (out-of-range ids included), and throw for a registered stream. The console oracle in prosper measured only that null return, with no stream registered, three argument patterns each and nothing written ([ACB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L48-L56), [DCB](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L196-L204)); the packet a registered stream gets is not measured. KytyPS5 has no body for these four, and its `IT_NOP` layout for the DCB active and complete calls differs from the SET_UCONFIG_REG, WRITE_DATA and SET_WORKLOAD sequence [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/agc_workload_packet.c) derives from the driver, so neither is taken. The `(buf, streamId)`, `(buf, streamId, workloadId)` and `(buf, streamId, workloadIds, workloadCount)` signatures are those of the DCB calls in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L2317-L2384), which says the ACB calls use the same driver builders; no ACB declaration was found. `sceAgcDcbSetWorkloadsActive` checks the stream before it reads the workload ids, as OpenAGC does: the console returned null for stream 1 with the ids at address 2 ([measured](https://github.com/mattias800/prosper/blob/df074498e56c0df22d74f9c6622e4709bc1c9a19/prosper/tests/data/console_oracle/agcbuild.golden.tsv#L203)) without faulting -- [sceAgcDcbSetWorkload*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console's packets are not written: the executor has no ATOMIC_MEM or per-stream mask table. In `libSceAgc.sprx` (firmware 12.00, read from disassembly) all six write a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete) and a WRITE_DATA to 0xC343, and active and complete add an ATOMIC_MEM (header `0xC0071E00`) on the stream's 64-bit mask. [Measured on a PS5](https://gist.github.com/orezraey/befe7632b827421988b47a6c04e89e1d) (firmware 13.60): WRITE_DATA dwords 5 to 8 are `0x06010000`, `0xC343`, 0 and the mask's high half (0 for inactive); the header low byte is 0x04 on the DCB SET_UCONFIG_REG and WRITE_DATA and 0x00 on the ACB ones; the table was at `0xFE003A000 + 8 * stream`, and the pointer given to `sceAgcDriverRegisterWorkloadStream` is never written +- [sceAgcDcbSetWorkload*](../../../core/libs/prx/libSceAgc/DcbState/src/Workload.cpp) and [sceAgcAcbSetWorkload*](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - the console's packets are not written: the executor has no ATOMIC_MEM or mask table. In `libSceAgc.sprx` (firmware 12.00, read from disassembly) all six write a SET_UCONFIG_REG to 0x342 (tag `0xCC000000 | stream`, or `0xCD000000 + workload * 32 + stream` for complete) and a WRITE_DATA to 0xC343, and active and complete add an ATOMIC_MEM (header `0xC0071E00`) on the stream's 64-bit mask. [Measured on a PS5](https://gist.github.com/orezraey/befe7632b827421988b47a6c04e89e1d) (firmware 13.60): WRITE_DATA dwords 5 to 8 are `0x06010000`, `0xC343`, 0 and the mask's high half (0 for inactive); the header low byte is 0x04 on the DCB SET_UCONFIG_REG and WRITE_DATA and 0x00 on the ACB ones; the table was at `0xFE003A000 + 8 * stream`; the pointer passed to `sceAgcDriverRegisterWorkloadStream` is never written - [sceAgcCreatePrimState](../../../core/libs/prx/libSceAgc/Shader/src/PrimState.cpp) (libSceAgc) - the `ge_user_vgpr_en` special is passed to the user configuration registers at whatever offset the shader header stores, as the SPRX does according to [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/3c7e8072f0d7684e930454794fd5c02056946d73/src/game_compat.c#L1181-L1252), which copies all four specials verbatim (KytyPS5 stops on any other offset). PPSA99039, PPSA99505, PPSA99515 and PPSA99997 build their headers by hand with it at 0x25c (`GE_USER_VGPR1`) instead of 0x262 (`GE_USER_VGPR_EN`), value 0, and run on the console; it is not moved to 0x262, and the driver reads neither register. `ge_cntl`, `vgt_shader_stages_en` and a used `vgt_gs_out_prim_type` must still be at their own offsets, since the driver reads those registers there, so a header that stores one elsewhere throws naming the offset, although by OpenAGC the SPRX would copy it as well - [sceAgcDcbEventWrite](../../../core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp) (libSceAgc) - non-sample-dump event types (any event other than 0x38 and 0x39) emit a 2-dword PM4 packet 0x46 without address dwords; the address argument is ignored for these events. Void Terrarium (PPSA03061, #5055) passes a non-null address buffer (0x00000002d2a10000) with event type 0x16 (cache flush). From 8a217a122409828e32f58df35f6200187dc046dd Mon Sep 17 00:00:00 2001 From: orezraey Date: Thu, 22 Oct 2026 02:52:40 -0300 Subject: [PATCH 161/318] docs(libkernel): record the unmeasured sceKernelCreateEventFlag error order Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6cdba1cc80..4fb32ed53b 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -60,6 +60,7 @@ - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console - [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. +- [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - when a call has both an invalid attribute and a name of 32 bytes or more, `SCE_KERNEL_ERROR_EINVAL` is returned, because the attribute checks run before the name length check. The console was only measured with long names and valid attributes (0x20 and 0x120), so which error it returns first is not known. Event flag handles are host addresses here, while the console returns small IDs. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the returned scheduling policy is unmeasured on a console. Both report the recorded guest policy, following [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L134-L164) From e0c67af31f40c1117d779f6c5cb96aeda31453c8 Mon Sep 17 00:00:00 2001 From: frankea Date: Thu, 22 Oct 2026 05:13:05 -0400 Subject: [PATCH 162/318] fix(agc): accept DB_EQAA INTERPOLATE_COMP_Z for a one-sample depth target without MSAA_ENABLE A one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is drawn as one sample at the pixel center, but DB_EQAA INTERPOLATE_COMP_Z threw on it. Gran Turismo 7 (PPSA01317) draws its 3840x2160 main pass this way after its EQAA pass (PA_SC_AA_CONFIG 0x0020c002, PA_SC_MODE_CNTL_0 0x2, DB_EQAA 0x00050000, a Z_32_FLOAT depth target with one sample) and stops on it. On this path every sample is taken to be at the pixel center, so Z interpolated to any sample position is the center's Z, which the one-sample draw already tests and writes. INTERPOLATE_COMP_Z is now accepted there. INTERPOLATE_SRC_Z and overrasterization still throw. Neither PAL nor radeonsi sets the bit and its meaning is not documented, so the debt entry records this as the same inference. --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 2 +- core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 9 +++++++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 44bc19a7d6..a875a68d6b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -1327,7 +1327,7 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { if (depthKeepsSampleZero && (read(cx, 0x292) & 1u) == 0) { const bool filled = result.topology != VK_PRIMITIVE_TOPOLOGY_POINT_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_LIST && result.topology != VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; const auto sampleInputs = read(cx, 0x1b3) & read(cx, 0x1b4) & (ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage)); - Require(filled && !result.alphaToCoverage && (PixelProgramSkipped(queue) || sampleInputs == 0) && (read(cx, 0x203) & 0x100u) == 0 && (read(cx, 0x201) & 0x0f0c0000u) == 0, "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA Z interpolation or overrasterization"); + Require(filled && !result.alphaToCoverage && (PixelProgramSkipped(queue) || sampleInputs == 0) && (read(cx, 0x203) & 0x100u) == 0 && (read(cx, 0x201) & 0x0f080000u) == 0, "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA source Z interpolation or overrasterization"); result.samples = 1; result.customSampleLocations = false; result.sampleLocations.fill(0); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 9f97985dcf..37c0446ee4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -831,7 +831,7 @@ void msaaEnableClearOneSampleTargetTests() { queue.context[0x1b3] = queue.context[0x1b4] = 2u | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::Ancillary) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::SampleCoverage); Require(AgcDriver::Graphics::PixelProgramSkipped(queue), "the depth-only draw did not skip its pixel program"); requireOneSampleDepth(queue, 0, "a one-sample depth target whose skipped pixel program has PS_ITER_SAMPLE and sample index and coverage inputs"); - constexpr std::string_view depthRefused = "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA Z interpolation or overrasterization"; + constexpr std::string_view depthRefused = "a one-sample depth target under a multisampled rasterizer without MSAA_ENABLE is unsupported for points or lines and with alpha-to-coverage, a sample index or coverage input, a sample-mask export, or DB_EQAA source Z interpolation or overrasterization"; for (const bool color : {false, true}) { for (const auto input : {ShaderRecompiler::PixelInput::Ancillary, ShaderRecompiler::PixelInput::SampleCoverage}) { queue = depthTarget(color); @@ -857,7 +857,12 @@ void msaaEnableClearOneSampleTargetTests() { queue = depthTarget(color); queue.userConfig[0x242] = 2; expectFailure([&] { DecodeState(queue); }, depthRefused); - for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { + for (const auto eqaa : {0x00040000u, 0x00050000u}) { + queue = depthTarget(color); + queue.context[0x201] = eqaa; + requireOneSampleDepth(queue, color ? 1u : 0u, "a one-sample depth target under a 4x PA_SC_AA_CONFIG without MSAA_ENABLE with DB_EQAA INTERPOLATE_COMP_Z"); + } + for (const auto eqaa : {0x00090002u, 0x000d0000u, 0x01010002u, 0x08010002u}) { queue = depthTarget(color); queue.context[0x201] = eqaa; expectFailure([&] { DecodeState(queue); }, depthRefused); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index baec5ded56..d1ae439ebf 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -32,7 +32,7 @@ - [Point-list geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 0` (point list) runs as a mesh shader with `OutputPoints` and scalar `PrimitivePointIndicesEXT`. Each primitive export emits one point whose vertex index is extracted from bits [8:0] of the packed word. A subgroup gets up to `maxVertices` points, with provoking vertex 0 and point raster primitive topology. Rectangle lists, per-stream types and point or line strips from a vertex program on the mesh path still throw. - [Line-strip geometry output](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - a geometry program with `VGT_GS_OUT_PRIM_TYPE = 1` (line strip) runs as a mesh shader with `OutputLinesEXT`: each primitive export is one line whose two vertex indices are packed like the three of a triangle, as Mesa's NGG lowering exports line strips (`ac_nir_lower_ngg_gs.c`), and each input primitive gets `VGT_GS_MAX_VERT_OUT - 1` lines where a triangle strip gets `- 2` triangles. A subgroup gets at least `GE_MAX_OUTPUT_PER_SUBGROUP` line slots; whether the console does the same is unmeasured. A fast-launch subgroup takes its line count from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR`. Line output counts as lines for polygon offset decode. The lines are drawn 1 pixel wide with Vulkan default rasterization; `PA_SU_LINE_CNTL` is not read, and console line rules are not measured. - [Alpha-to-coverage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - only `DB_ALPHA_TO_MASK = 0xaa01` is accepted: the enable bit with the four quad offsets at 2 and no `OFFSET_ROUND`, the undithered setting [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/bffd91947efc1932da99aca69fc540d6d4201b0c/src/gallium/drivers/radeonsi/si_state.c#L371-L381) programs. Draws are single-sampled, so the pixel shader [writes](../../../core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp) `SampleMask` 0 when its MRT0 alpha is below one half or NaN and all ones otherwise, and runs with late fragment tests. That rule was measured on a Radeon 660M (gfx1035, RADV with `RADV_DEBUG=noatocdithering`, which programs the same register) for 0, -0, 0.25, the two floats below one half, one half, the two above, 0.75, 1, 2, -1, both infinities and both NaNs, and is pinned in [AlphaToCoverage.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/AlphaToCoverage.cpp); it is not measured on gfx1013. Vulkan's `alphaToCoverageEnable` is not used: the NVIDIA driver 615.78.08 dithers single-sample coverage (an alpha of 0.25 writes half the pixels of an RTX 3050 Laptop target). Other offsets, `OFFSET_ROUND`, a sample-mask export, and an MRT0 that is missing, swizzled, packed or exported without a floating-point alpha throw. A draw without a color target on MRT0 keeps alpha-to-coverage from its MRT0 export, as [radeonsi exports MRT0 as `32_AR` for alpha-to-coverage without a color buffer](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state_shaders.cpp#L2425-L2431). A draw that enables alpha-to-coverage with no MRT0 export still throws: what the console covers then is not measured. Default dithered `DB_ALPHA_TO_MASK = 0x18701` additionally uses a per-quad sample-mask table [measured on gfx1036](https://github.com/boykopovar/AnyPS5/pull/4451#issuecomment-6124848921) for eight standard samples with FP16_ABGR or 32_ABGR exports; other sample counts, custom locations, EQAA combinations and dithered UNORM16/SNORM16 exports throw. The dithered rule is not measured on gfx1013. -- [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, and so does `PS_ITER_SAMPLE` with one-sample color targets and no depth target, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. With a one-sample depth target, a pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points, lines and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA` Z interpolation (`INTERPOLATE_COMP_Z`, `INTERPOLATE_SRC_Z`) or overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) also throws. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. +- [A one-sample target under a multisampled rasterizer without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear, one-sample color targets and no depth target, or a one-sample depth target with no color target or one-sample color targets, is drawn as a one-sample draw at the pixel center. Without the bit every sample is taken to be at the pixel center (see the entry for a multisampled target rasterized without MSAA_ENABLE below), and a one-sample target is taken to keep the coverage of sample 0, so a one-sample depth target is tested and written once per pixel with the depth at the pixel center. That is the rule [`VK_AMD_mixed_attachment_samples`](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/fragops.adoc#L2510-L2516) gives a color target with fewer samples than the rasterizer, but Vulkan reaches it only beside a depth or stencil target with the rasterizer's sample count ([VUID-VkGraphicsPipelineCreateInfo-subpass-01505](https://github.com/KhronosGroup/Vulkan-Docs/blob/193ebe195f47ffc5004683d1659e71c725658c4d/chapters/pipelines.adoc#L3033-L3041)), so here it is an analogy, and for a one-sample depth target an extension of it. [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp#L127-L128) also rasterizes with the largest sample count of the bound depth and color targets ([L416-L417](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L416-L417), [L477-L480](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L477-L480)), and reads `PA_SC_AA_CONFIG` only for a `SAMPLE_COVERAGE` input ([L174-L177](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp#L174-L177)). This is not measured on hardware. An `ANCILLARY` or `SAMPLE_COVERAGE` pixel input and a sample-mask export throw in a pixel program that runs, and so does `PS_ITER_SAMPLE` with one-sample color targets and no depth target, because what the console gives them under a multisampled configuration is not known; a depth-only draw that skips its pixel program is drawn with the null program, and its `PS_ITER_SAMPLE` and pixel input bits are ignored. With a one-sample depth target, a pixel program with `PS_ITER_SAMPLE` and neither input runs once per pixel; if the console runs it once per sample, its atomics and append or consume counters count differently. Points, lines and alpha-to-coverage throw as for a multisampled target. With a one-sample depth target, `DB_EQAA.INTERPOLATE_COMP_Z` is accepted: with every sample at the pixel center, Z interpolated to any sample position is the center's Z, which the one-sample draw tests. That rests on the same inference; neither [PAL](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MsaaState.cpp#L117-L118) nor [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/761d5c8a54f93b3d43608f718910ed742983dd0b/src/gallium/drivers/radeonsi/si_state.c#L3314-L3316) sets the bit, and its meaning is not documented. `INTERPOLATE_SRC_Z` and overrasterization (`OVERRASTERIZATION_AMOUNT`, `ENABLE_POSTZ_OVERRASTERIZATION`) still throw. METAL GEAR SOLID: Peace Walker (PPSA18216) draws a one-sample color target under a 4x configuration this way; the first draw of its opening movie binds a one-sample depth target and no color target under the same configuration. Gran Turismo 7 (PPSA01317) draws its main pass with a one-sample depth target, `DB_EQAA` 0x00050000 and a 4x configuration. - [MSAA_ENABLE on a single-sample target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_MODE_CNTL_0.MSAA_ENABLE` with `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES = 0` is drawn like the same state without the bit when sample 0 of the four `PA_SC_AA_SAMPLE_LOCS_PIXEL_*_0` registers is at the pixel center and the draw rasterizes filled triangles or rectangles (`VGT_PRIMITIVE_TYPE` 7 or 17 as well as the triangle types) with alpha-to-coverage and conservative rasterization off. [RADV](https://gitlab.freedesktop.org/mesa/mesa/-/blob/745709a000482a4a24c91faef5c5a40bd653a863/src/amd/vulkan/radv_cmd_buffer.c#L13606-13609) programs this state for one-sample custom sample locations on GFX10 and later, where the bit makes the rasterizer use the programmed location of sample 0 ([061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)); that the bit changes nothing else for such a draw is inferred from that and is not measured on hardware. EA Sports UFC 5 (PPSA03541) reaches a rect-list draw in this state. Other locations of sample 0, absent location registers, points, lines, alpha-to-coverage and conservative rasterization throw - [A multisampled target rasterized without MSAA_ENABLE](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES` > 0 with `PA_SC_MODE_CNTL_0.MSAA_ENABLE` clear is drawn with the target's sample count and every sample at the pixel center, as a single-sample scan into a multisampled target already is. On GFX10 and later the bit is what makes the rasterizer use the programmed sample locations ([RADV 061bc615](https://gitlab.freedesktop.org/mesa/mesa/-/commit/061bc6151a11597f720535f00e79a5752e24aff6)), and with multisampling disabled GL rasterizes at the pixel center and gives the fragment full coverage, which radeonsi programs by clearing the bit; that the hardware does the same with `MSAA_NUM_SAMPLES` > 0 is inferred from these and is not measured on hardware. A depth target with the same sample count also has every sample at the pixel center, so each depth and stencil sample is tested and written with the depth at the pixel center, as GL allows when multisampling is disabled ([L185-L190](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L185-L190), [L422-L425](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L422-L425)). GL also lets an implementation test only a centermost sample and set every sample from it ([L425-L434](https://github.com/KhronosGroup/OpenGL-Registry/blob/6af574a14089ccfee87efe230ebcdd8742859813/extensions/ARB/ARB_multisample.txt#L425-L434)), which differs when a pixel's stored samples differ; which one the console does, and how its occlusion queries count such a draw, is not measured on hardware. `DB_EQAA` Z interpolation or overrasterization and a sample-mask export with a depth target throw. Points, lines and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target, and METAL GEAR SOLID: Peace Walker (PPSA18216) a 4x depth and color pair. - [sceAgcDriverGetEqContextId](../../../core/libs/prx/libSceAgcDriver/Eq/src/Query.cpp) (libSceAgcDriver) - returns `ident`: an end-of-pipe interrupt is delivered only to the registration whose id equals the raising queue, so `ident` is that context's id (titles register 0 or 1 for graphics and `0x20 + 8 * pipe + queue` for compute). The console's value is not known; KytyPS5 returns the event's `data`, which it fills with the interrupt's context id, whereas `data` here is the EV_CLEAR trigger count. Other event filters throw From c27787a7db406ddff2872479fc1030b5dc6fff29 Mon Sep 17 00:00:00 2001 From: mugurc Date: Mon, 12 Oct 2026 15:12:03 +0300 Subject: [PATCH 163/318] fix(libc): build the host-facing libc code on macOS feof_unlocked, ferror_unlocked, getc_unlocked and putc_unlocked are macros on macOS, so they are called without the global scope qualifier. The remaining differences are the ones Darwin's headers and libSystem have: - CLOCK_REALTIME_COARSE and st_mtim do not exist; the write mark uses CLOCK_REALTIME and st_mtimespec. - tm_zone is a non-const char*. - __cxa_thread_atexit returns void, so the wrapper reports success. - Darwin's stack protector reads __stack_chk_guard from libSystem; the guard of the guest libc has the postfixed name, which receives the NID of the original. - APS5_EXPORT spells the symbols with the leading underscore of Mach-O. - GuestArenaSetProtection and PosixProtection are built on macOS as on Linux. - The fclose failure test fills /dev/full, which exists on Linux only. exception_runtime relies on the Itanium unwinder of libc, which is not registered on macOS (the host libc++abi is used), so its test is only registered elsewhere. --- core/libs/prx/libc/CMakeLists.txt | 8 +++---- core/libs/prx/libc/include/FileStream.hpp | 4 ++-- .../prx/libc/include/general/ExportMacros.hpp | 6 ++++- core/libs/prx/libc/src/CxxAbiSupport.cpp | 22 +++++++++++-------- core/libs/prx/libc/src/General.cpp | 8 +++++++ core/libs/prx/libc/src/StandardStreams.cpp | 4 ++-- core/libs/prx/libc/src/Time.cpp | 4 ++++ .../prx/libc/src/specifics/gcc/StackGuard.cpp | 4 ++++ core/libs/tests/Formatting.cpp | 7 ++++++ core/libs/tests/GuestFiopen.cpp | 2 +- 10 files changed, 50 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index d53c2d0461..68e970b46d 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -817,10 +817,6 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() if(UNIX) - add_test_executable(exception_runtime_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionRuntime.cpp) - target_include_directories(exception_runtime_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(exception_runtime_tests PRIVATE libc) - add_test(NAME exception_runtime COMMAND exception_runtime_tests) add_test_executable(exception_personality_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionPersonality.cpp) target_include_directories(exception_personality_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(exception_personality_tests PRIVATE libc) @@ -835,6 +831,10 @@ if(BUILD_TESTING) add_test(NAME guest_path_lifetime COMMAND guest_path_lifetime_tests) set_tests_properties(guest_path_lifetime PROPERTIES TIMEOUT 20) if(NOT APPLE) + add_test_executable(exception_runtime_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionRuntime.cpp) + target_include_directories(exception_runtime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(exception_runtime_tests PRIVATE libc) + add_test(NAME exception_runtime COMMAND exception_runtime_tests) enable_language(C) add_test_executable(exception_c_cleanup_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.cpp ${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.c) set_source_files_properties(${LIBS_INCLUDE_DIR}/tests/ExceptionCCleanup.c PROPERTIES COMPILE_OPTIONS -fexceptions) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 7a81d29f07..1056a6d4c6 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -228,8 +228,8 @@ public: const bool endOfFile = std::feof(GetHandle()) != 0; const bool error = std::ferror(GetHandle()) != 0; #else - const bool endOfFile = ::feof_unlocked(GetHandle()) != 0; - const bool error = ::ferror_unlocked(GetHandle()) != 0; + const bool endOfFile = feof_unlocked(GetHandle()) != 0; + const bool error = ferror_unlocked(GetHandle()) != 0; #endif _guest.flags = static_cast((_guest.flags & ~0x60) | (endOfFile ? 0x20 : 0) | (error || guestError ? 0x40 : 0)); diff --git a/core/libs/prx/libc/include/general/ExportMacros.hpp b/core/libs/prx/libc/include/general/ExportMacros.hpp index 49ab7fbf8c..c175c74f2b 100644 --- a/core/libs/prx/libc/include/general/ExportMacros.hpp +++ b/core/libs/prx/libc/include/general/ExportMacros.hpp @@ -1,7 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_EXPORTMACROS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_GENERAL_EXPORTMACROS_HPP -#if defined(__GNUC__) || defined(__clang__) +#if defined(__APPLE__) + #define APS5_EXPORT(exportName, funcName) \ + __asm__(".globl \"_" exportName "_nid_no_patch_cut\"\n\t" \ + ".set \"_" exportName "_nid_no_patch_cut\", _" #funcName) +#elif defined(__GNUC__) || defined(__clang__) #define APS5_EXPORT(exportName, funcName) \ __asm__(".globl \"" exportName "_nid_no_patch_cut\"\n\t" \ ".set \"" exportName "_nid_no_patch_cut\", " #funcName) diff --git a/core/libs/prx/libc/src/CxxAbiSupport.cpp b/core/libs/prx/libc/src/CxxAbiSupport.cpp index 1cbe9c38cd..ccecea01a8 100644 --- a/core/libs/prx/libc/src/CxxAbiSupport.cpp +++ b/core/libs/prx/libc/src/CxxAbiSupport.cpp @@ -74,18 +74,22 @@ int APS5_VABI __cxa_thread_atexit_impl_nid_postfix(void (APS5_VABI *func)(void*) auto* context = new (std::nothrow) ThreadAtexitContext{func, arg}; if (context == nullptr) return -1; - const int result = __cxxabiv1::__cxa_thread_atexit( - [](void* opaque) { - auto* context = static_cast(opaque); - const auto destructor = context->destructor; - void* object = context->object; - delete context; - destructor(object); - }, - context, dso); + const auto run = [](void* opaque) { + auto* context = static_cast(opaque); + const auto destructor = context->destructor; + void* object = context->object; + delete context; + destructor(object); + }; +#ifdef __APPLE__ + __cxxabiv1::__cxa_thread_atexit(run, context, dso); + return 0; +#else + const int result = __cxxabiv1::__cxa_thread_atexit(run, context, dso); if (result != 0) delete context; return result; +#endif } int APS5_VABI __cxa_thread_atexit_nid_postfix(void (APS5_VABI *func)(void*), void* arg, void* dso) { diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 26c1935c5a..6bbe2aafc7 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -376,7 +376,11 @@ bool NotBefore(const WriteMark& time, const WriteMark& mark) { using WriteMark = timespec; WriteMark Now() { timespec now; +#ifdef __APPLE__ + clock_gettime(CLOCK_REALTIME, &now); +#else clock_gettime(CLOCK_REALTIME_COARSE, &now); +#endif return now; } bool NotBefore(const WriteMark& time, const WriteMark& mark) { @@ -415,7 +419,11 @@ void FlushPath(const std::filesystem::path& path) { #else std::optional WriteTime(const std::filesystem::path& path) { struct stat data; +#ifdef __APPLE__ + if (::stat(path.c_str(), &data) == 0) return data.st_mtimespec; +#else if (::stat(path.c_str(), &data) == 0) return data.st_mtim; +#endif if (errno == ENOENT || errno == ENOTDIR) return std::nullopt; throw std::system_error(errno, std::generic_category(), "Reading the write time of " + path.string()); } diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 2cd656f01f..4d8c34a019 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -92,7 +92,7 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { #ifdef _WIN32 const int result = _fgetc_nolock(native); #else - const int result = ::getc_unlocked(native); + const int result = getc_unlocked(native); #endif stream->SyncUnlockedStatus(); stream->KeepDirectoryReadError(result == -1); @@ -115,7 +115,7 @@ int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { #ifdef _WIN32 const int result = _fputc_nolock(value, GetNativeStream(stream)); #else - const int result = ::putc_unlocked(value, GetNativeStream(stream)); + const int result = putc_unlocked(value, GetNativeStream(stream)); #endif stream->SyncUnlockedStatus(); return result; diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index c3499f8047..f732111f27 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -101,7 +101,11 @@ std::tm toHostTm(const GuestTm& guest) { host.tm_isdst = guest.tm_isdst; #ifndef _WIN32 host.tm_gmtoff = static_cast(guest.tm_gmtoff); +#ifdef __APPLE__ + host.tm_zone = const_cast(guest.tm_zone); +#else host.tm_zone = guest.tm_zone; +#endif #endif return host; } diff --git a/core/libs/prx/libc/src/specifics/gcc/StackGuard.cpp b/core/libs/prx/libc/src/specifics/gcc/StackGuard.cpp index bae022e001..a18ff70dc9 100644 --- a/core/libs/prx/libc/src/specifics/gcc/StackGuard.cpp +++ b/core/libs/prx/libc/src/specifics/gcc/StackGuard.cpp @@ -1,4 +1,8 @@ #include #include +#ifdef __APPLE__ +extern "C" std::uintptr_t __stack_chk_guard_nid_postfix = 0xDEADBEEFCAFEBABEull; +#else std::uintptr_t __stack_chk_guard = 0xDEADBEEFCAFEBABEull; +#endif diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index f22d7880dd..987a120d95 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -10,6 +10,13 @@ #include #include #include +#include + +#if defined(__clang__) && !defined(_WIN32) +#define __builtin_sysv_va_list va_list +#define __builtin_sysv_va_start va_start +#define __builtin_sysv_va_end va_end +#endif extern "C" { int APS5_VABI snprintf_nid_postfix(char*, size_t, const char*, ...); diff --git a/core/libs/tests/GuestFiopen.cpp b/core/libs/tests/GuestFiopen.cpp index 276459ec07..044265f11c 100644 --- a/core/libs/tests/GuestFiopen.cpp +++ b/core/libs/tests/GuestFiopen.cpp @@ -152,7 +152,7 @@ int main() { Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'k'); fclose_nid_postfix(stream); -#ifndef _WIN32 +#ifdef __linux__ auto* full = std::fopen("/dev/full", "wb"); Require(full != nullptr); auto* fullStream = new FileStream(full, true); From 6e6c15962a2ee2db799aae74bbe38f6aeed5def4 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:45:48 +0300 Subject: [PATCH 164/318] fix(shader): fold constant composite tables localized to Function storage --- .../prx/libSceAgcDriver/tests/SpirvModule.cpp | 154 +++++++++--------- .../libSceAgcDriver/tests/SpirvOptimizer.cpp | 103 ++++++------ .../SpirvBackend/src/SpirvSpecialization.cpp | 32 ++-- 3 files changed, 151 insertions(+), 138 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp index 00796cd2b5..06de269deb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -299,82 +299,88 @@ void testIdsBeyondTheBoundAreRejected() { } void testPrivateCompositeTableLoadFolding() { - for (const auto accessOp : {spv::OpAccessChain, spv::OpInBoundsAccessChain}) { - const auto build = [&](bool dynamicIndex, bool storeThroughChain) { - SpirvModule module; - module.EmitCapability(spv::CapabilityShader); - const auto typeVoid = module.Type(spv::OpTypeVoid); - const auto typeBool = module.Type(spv::OpTypeBool); - const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); - const auto falseVal = module.Constant(spv::OpConstantFalse, typeBool); - const auto zero = module.Constant(spv::OpConstant, typeU32, 0u); - const auto one = module.Constant(spv::OpConstant, typeU32, 1u); - const auto three = module.Constant(spv::OpConstant, typeU32, 3u); - const auto sixteen = module.Constant(spv::OpConstant, typeU32, 16u); - const auto twenty = module.Constant(spv::OpConstant, typeU32, 20u); - const auto thirty = module.Constant(spv::OpConstant, typeU32, 30u); - const auto typeArray = module.Type(spv::OpTypeArray, typeU32, three); - const auto initial = module.Constant(spv::OpConstantComposite, typeArray, std::vector{sixteen, twenty, thirty}); - const auto typeInputPtr = module.Type(spv::OpTypePointer, spv::StorageClassInput, typeU32); - const auto typeTablePtr = module.Type(spv::OpTypePointer, spv::StorageClassPrivate, typeArray); - const auto typePrivatePtr = module.Type(spv::OpTypePointer, spv::StorageClassPrivate, typeU32); - const auto typeFunction = module.Type(spv::OpTypeFunction, typeVoid); - const auto input = module.DefineGlobalVariable(typeInputPtr, spv::StorageClassInput); - const auto table = module.DefineInitializedGlobalVariable(typeTablePtr, spv::StorageClassPrivate, initial); - const auto output = module.DefineGlobalVariable(typePrivatePtr, spv::StorageClassPrivate); - const auto mainId = module.AllocateId(); - const auto lane = module.AllocateId(); - const auto rawCond = module.AllocateId(); - const auto cond = module.AllocateId(); - const auto baseIndex = module.AllocateId(); - const auto index = module.AllocateId(); - const auto element = module.AllocateId(); - const auto loaded = module.AllocateId(); - const auto sum = module.AllocateId(); - module.AddFunction(spv::OpFunction, typeVoid, mainId, spv::FunctionControlMaskNone, typeFunction); - module.AddFunction(spv::OpLabel, module.AllocateId()); - module.AddFunction(spv::OpLoad, typeU32, lane, input); - if (storeThroughChain) { - const auto writePtr = module.AllocateId(); - module.AddFunction(accessOp, typePrivatePtr, writePtr, table, zero); - module.AddFunction(spv::OpStore, writePtr, lane); + for (const auto storageClass : {spv::StorageClassPrivate, spv::StorageClassFunction}) { + for (const auto accessOp : {spv::OpAccessChain, spv::OpInBoundsAccessChain}) { + const auto build = [&](bool dynamicIndex, bool storeThroughChain) { + SpirvModule module; + module.EmitCapability(spv::CapabilityShader); + const auto typeVoid = module.Type(spv::OpTypeVoid); + const auto typeBool = module.Type(spv::OpTypeBool); + const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u); + const auto falseVal = module.Constant(spv::OpConstantFalse, typeBool); + const auto zero = module.Constant(spv::OpConstant, typeU32, 0u); + const auto one = module.Constant(spv::OpConstant, typeU32, 1u); + const auto three = module.Constant(spv::OpConstant, typeU32, 3u); + const auto sixteen = module.Constant(spv::OpConstant, typeU32, 16u); + const auto twenty = module.Constant(spv::OpConstant, typeU32, 20u); + const auto thirty = module.Constant(spv::OpConstant, typeU32, 30u); + const auto typeArray = module.Type(spv::OpTypeArray, typeU32, three); + const auto initial = module.Constant(spv::OpConstantComposite, typeArray, std::vector{sixteen, twenty, thirty}); + const auto typeInputPtr = module.Type(spv::OpTypePointer, spv::StorageClassInput, typeU32); + const auto typeTablePtr = module.Type(spv::OpTypePointer, storageClass, typeArray); + const auto typeElementPtr = module.Type(spv::OpTypePointer, storageClass, typeU32); + const auto typePrivatePtr = module.Type(spv::OpTypePointer, spv::StorageClassPrivate, typeU32); + const auto typeFunction = module.Type(spv::OpTypeFunction, typeVoid); + const auto input = module.DefineGlobalVariable(typeInputPtr, spv::StorageClassInput); + const auto table = storageClass == spv::StorageClassPrivate + ? module.DefineInitializedGlobalVariable(typeTablePtr, spv::StorageClassPrivate, initial) + : module.AllocateId(); + const auto output = module.DefineGlobalVariable(typePrivatePtr, spv::StorageClassPrivate); + const auto mainId = module.AllocateId(); + const auto lane = module.AllocateId(); + const auto rawCond = module.AllocateId(); + const auto cond = module.AllocateId(); + const auto baseIndex = module.AllocateId(); + const auto index = module.AllocateId(); + const auto element = module.AllocateId(); + const auto loaded = module.AllocateId(); + const auto sum = module.AllocateId(); + module.AddFunction(spv::OpFunction, typeVoid, mainId, spv::FunctionControlMaskNone, typeFunction); + module.AddFunction(spv::OpLabel, module.AllocateId()); + if (storageClass == spv::StorageClassFunction) module.AddFunction(spv::OpVariable, typeTablePtr, table, spv::StorageClassFunction, initial); + module.AddFunction(spv::OpLoad, typeU32, lane, input); + if (storeThroughChain) { + const auto writePtr = module.AllocateId(); + module.AddFunction(accessOp, typeElementPtr, writePtr, table, zero); + module.AddFunction(spv::OpStore, writePtr, lane); + } + module.AddFunction(spv::OpUGreaterThan, typeBool, rawCond, lane, zero); + module.AddFunction(spv::OpLogicalOr, typeBool, cond, rawCond, falseVal); + module.AddFunction(spv::OpIAdd, typeU32, baseIndex, one, one); + module.AddFunction(spv::OpSelect, typeU32, index, cond, baseIndex, baseIndex); + module.AddFunction(accessOp, typeElementPtr, element, table, dynamicIndex ? lane : index); + module.AddFunction(spv::OpLoad, typeU32, loaded, element); + module.AddFunction(spv::OpIAdd, typeU32, sum, lane, loaded); + module.AddFunction(spv::OpStore, output, sum); + module.AddFunction(spv::OpReturn); + module.AddFunction(spv::OpFunctionEnd); + return std::pair{ShaderRecompiler::SpecializeSpirv(module.Finalize()), thirty}; + }; + const auto [folded, expectedConstant] = build(false, false); + const auto foldedCounts = opcodeCounts(folded); + check(!foldedCounts.contains(accessOp), "a constant-index composite table access chain was not removed"); + check(!foldedCounts.contains(spv::OpLogicalOr), "a right-false OpLogicalOr was not simplified"); + check(!foldedCounts.contains(spv::OpSelect), "an identical-arm OpSelect was not simplified"); + check(foldedCounts.contains(spv::OpLoad) && foldedCounts.at(spv::OpLoad) == 1u, "a constant-index composite table load was not folded"); + bool addedThirty = false; + for (std::size_t cursor = 5; cursor < folded.size(); cursor += folded[cursor] >> 16u) { + if ((folded[cursor] >> 16u) == 0u) break; + if ((folded[cursor] & 0xffffu) == spv::OpIAdd && folded[cursor + 4u] == expectedConstant) addedThirty = true; } - module.AddFunction(spv::OpUGreaterThan, typeBool, rawCond, lane, zero); - module.AddFunction(spv::OpLogicalOr, typeBool, cond, rawCond, falseVal); - module.AddFunction(spv::OpIAdd, typeU32, baseIndex, one, one); - module.AddFunction(spv::OpSelect, typeU32, index, cond, baseIndex, baseIndex); - module.AddFunction(accessOp, typePrivatePtr, element, table, dynamicIndex ? lane : index); - module.AddFunction(spv::OpLoad, typeU32, loaded, element); - module.AddFunction(spv::OpIAdd, typeU32, sum, lane, loaded); - module.AddFunction(spv::OpStore, output, sum); - module.AddFunction(spv::OpReturn); - module.AddFunction(spv::OpFunctionEnd); - return std::pair{ShaderRecompiler::SpecializeSpirv(module.Finalize()), thirty}; - }; - const auto [folded, expectedConstant] = build(false, false); - const auto foldedCounts = opcodeCounts(folded); - check(!foldedCounts.contains(accessOp), "a constant-index private composite table access chain was not removed"); - check(!foldedCounts.contains(spv::OpLogicalOr), "a right-false OpLogicalOr was not simplified"); - check(!foldedCounts.contains(spv::OpSelect), "an identical-arm OpSelect was not simplified"); - check(foldedCounts.contains(spv::OpLoad) && foldedCounts.at(spv::OpLoad) == 1u, "a constant-index private composite table load was not folded"); - bool addedThirty = false; - for (std::size_t cursor = 5; cursor < folded.size(); cursor += folded[cursor] >> 16u) { - if ((folded[cursor] >> 16u) == 0u) break; - if ((folded[cursor] & 0xffffu) == spv::OpIAdd && folded[cursor + 4u] == expectedConstant) addedThirty = true; + check(addedThirty, "a constant-index composite table load did not fold to its element constant"); + + const auto [dynamic, unusedDynamic] = build(true, false); + static_cast(unusedDynamic); + const auto dynamicCounts = opcodeCounts(dynamic); + check(dynamicCounts.contains(accessOp) && dynamicCounts.at(accessOp) == 1u, "a dynamic-index composite table access chain was removed"); + check(dynamicCounts.contains(spv::OpLoad) && dynamicCounts.at(spv::OpLoad) == 2u, "a dynamic-index composite table load was folded"); + + const auto [mutated, unusedMutated] = build(false, true); + static_cast(unusedMutated); + const auto mutatedCounts = opcodeCounts(mutated); + check(mutatedCounts.contains(accessOp) && mutatedCounts.at(accessOp) == 2u, "a mutated composite table access chain was removed"); + check(mutatedCounts.contains(spv::OpLoad) && mutatedCounts.at(spv::OpLoad) == 2u, "a mutated composite table load was folded despite a store through an access chain"); } - check(addedThirty, "a constant-index private composite table load did not fold to its element constant"); - - const auto [dynamic, unusedDynamic] = build(true, false); - static_cast(unusedDynamic); - const auto dynamicCounts = opcodeCounts(dynamic); - check(dynamicCounts.contains(accessOp) && dynamicCounts.at(accessOp) == 1u, "a dynamic-index private composite table access chain was removed"); - check(dynamicCounts.contains(spv::OpLoad) && dynamicCounts.at(spv::OpLoad) == 2u, "a dynamic-index private composite table load was folded"); - - const auto [mutated, unusedMutated] = build(false, true); - static_cast(unusedMutated); - const auto mutatedCounts = opcodeCounts(mutated); - check(mutatedCounts.contains(accessOp) && mutatedCounts.at(accessOp) == 2u, "a mutated private composite table access chain was removed"); - check(mutatedCounts.contains(spv::OpLoad) && mutatedCounts.at(spv::OpLoad) == 2u, "a mutated private composite table load was folded despite a store through an access chain"); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp index 9534ce7716..b50010c578 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp @@ -396,6 +396,7 @@ std::optional> storedLaneFunction(const switch (opcode) { case spv::OpFunction: case spv::OpLabel: + case spv::OpVariable: case spv::OpReturn: case spv::OpFunctionEnd: break; @@ -936,7 +937,10 @@ OpStore %slot2 %second check(opcodeCount(acrossCall, spv::OpGroupNonUniformBallot) == 2, "a ballot was shared across a function call"); } -std::string privateTableModuleSource(const std::string& body) { +std::string privateTableModuleSource(const std::string& body, bool functionTable = false) { + const std::string storage = functionTable ? "Function" : "Private"; + const std::string globalTable = functionTable ? "" : "%table = OpVariable %tableArray Private %initial\n"; + const std::string localTable = functionTable ? "%table = OpVariable %tableArray Function %initial\n" : ""; return R"(OpCapability Shader OpMemoryModel Logical GLSL450 OpEntryPoint GLCompute %main "main" %index @@ -955,66 +959,69 @@ OpDecorate %index BuiltIn LocalInvocationIndex %array = OpTypeArray %uint %three %initial = OpConstantComposite %array %sixteen %twenty %thirty %input = OpTypePointer Input %uint -%privateArray = OpTypePointer Private %array +%tableArray = OpTypePointer )" + storage + R"( %array +%tableElement = OpTypePointer )" + storage + R"( %uint %private = OpTypePointer Private %uint %function = OpTypeFunction %void %index = OpVariable %input Input -%table = OpVariable %privateArray Private %initial -%output = OpVariable %private Private +)" + globalTable + R"(%output = OpVariable %private Private %main = OpFunction %void None %function %entry = OpLabel -%lane = OpLoad %uint %index +)" + localTable + R"(%lane = OpLoad %uint %index )" + body + R"(OpReturn OpFunctionEnd )"; } void testSpecializedPrivateCompositeTableLoad() { - for (const bool inBounds : {false, true}) { - const auto accessName = inBounds ? "OpInBoundsAccessChain" : "OpAccessChain"; - const auto accessOp = inBounds ? spv::OpInBoundsAccessChain : spv::OpAccessChain; - try { - const auto folded = assemble(privateTableModuleSource( - "%rawCond = OpUGreaterThan %bool %lane %zero\n" - "%cond = OpLogicalOr %bool %rawCond %false\n" - "%baseOffset = OpIAdd %uint %one %one\n" - "%offset = OpSelect %uint %cond %baseOffset %baseOffset\n" - "%element = " + std::string(accessName) + " %private %table %offset\n" - "%loaded = OpLoad %uint %element\n" - "%sum = OpIAdd %uint %lane %loaded\n" - "OpStore %output %sum\n")); - static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(folded, Vulkan11, Spirv13, false, false)); - const auto specializedFolded = ShaderRecompiler::SpecializeSpirv(folded); - static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedFolded, Vulkan11, Spirv13, false, false)); - check(opcodeCount(specializedFolded, accessOp) == 0u, std::string(accessName) + ": constant-index private table access chain was not removed"); - check(opcodeCount(specializedFolded, spv::OpLogicalOr) == 0u, std::string(accessName) + ": right-false OpLogicalOr was not simplified"); - check(opcodeCount(specializedFolded, spv::OpSelect) == 0u, std::string(accessName) + ": identical-arm OpSelect was not simplified"); - check(opcodeCount(specializedFolded, spv::OpLoad) == 1u, std::string(accessName) + ": constant-index private table load was not folded"); - check(storedLaneFunction(specializedFolded) == std::optional>({1u, 30u}), std::string(accessName) + ": constant-index private table load folded to the wrong value"); + for (const bool functionTable : {false, true}) { + for (const bool inBounds : {false, true}) { + const auto accessName = inBounds ? "OpInBoundsAccessChain" : "OpAccessChain"; + const auto accessOp = inBounds ? spv::OpInBoundsAccessChain : spv::OpAccessChain; + const std::string label = std::string(accessName) + (functionTable ? " (Function)" : " (Private)"); + try { + const auto folded = assemble(privateTableModuleSource( + "%rawCond = OpUGreaterThan %bool %lane %zero\n" + "%cond = OpLogicalOr %bool %rawCond %false\n" + "%baseOffset = OpIAdd %uint %one %one\n" + "%offset = OpSelect %uint %cond %baseOffset %baseOffset\n" + "%element = " + std::string(accessName) + " %tableElement %table %offset\n" + "%loaded = OpLoad %uint %element\n" + "%sum = OpIAdd %uint %lane %loaded\n" + "OpStore %output %sum\n", functionTable)); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(folded, Vulkan11, Spirv13, false, false)); + const auto specializedFolded = ShaderRecompiler::SpecializeSpirv(folded); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedFolded, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedFolded, accessOp) == 0u, label + ": constant-index table access chain was not removed"); + check(opcodeCount(specializedFolded, spv::OpLogicalOr) == 0u, label + ": right-false OpLogicalOr was not simplified"); + check(opcodeCount(specializedFolded, spv::OpSelect) == 0u, label + ": identical-arm OpSelect was not simplified"); + check(opcodeCount(specializedFolded, spv::OpLoad) == 1u, label + ": constant-index table load was not folded"); + check(storedLaneFunction(specializedFolded) == std::optional>({1u, 30u}), label + ": constant-index table load folded to the wrong value"); - const auto dynamic = assemble(privateTableModuleSource( - "%element = " + std::string(accessName) + " %private %table %lane\n" - "%loaded = OpLoad %uint %element\n" - "OpStore %output %loaded\n")); - static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(dynamic, Vulkan11, Spirv13, false, false)); - const auto specializedDynamic = ShaderRecompiler::SpecializeSpirv(dynamic); - static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedDynamic, Vulkan11, Spirv13, false, false)); - check(opcodeCount(specializedDynamic, accessOp) == 1u, std::string(accessName) + ": dynamic-index private table access chain was removed"); - check(opcodeCount(specializedDynamic, spv::OpLoad) == 2u, std::string(accessName) + ": dynamic-index private table load was folded"); + const auto dynamic = assemble(privateTableModuleSource( + "%element = " + std::string(accessName) + " %tableElement %table %lane\n" + "%loaded = OpLoad %uint %element\n" + "OpStore %output %loaded\n", functionTable)); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(dynamic, Vulkan11, Spirv13, false, false)); + const auto specializedDynamic = ShaderRecompiler::SpecializeSpirv(dynamic); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedDynamic, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedDynamic, accessOp) == 1u, label + ": dynamic-index table access chain was removed"); + check(opcodeCount(specializedDynamic, spv::OpLoad) == 2u, label + ": dynamic-index table load was folded"); - const auto mutated = assemble(privateTableModuleSource( - "%writeElement = " + std::string(accessName) + " %private %table %zero\n" - "OpStore %writeElement %lane\n" - "%readElement = " + std::string(accessName) + " %private %table %one\n" - "%loaded = OpLoad %uint %readElement\n" - "OpStore %output %loaded\n")); - static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(mutated, Vulkan11, Spirv13, false, false)); - const auto specializedMutated = ShaderRecompiler::SpecializeSpirv(mutated); - static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedMutated, Vulkan11, Spirv13, false, false)); - check(opcodeCount(specializedMutated, accessOp) == 2u, std::string(accessName) + ": mutated private table access chain was removed"); - check(opcodeCount(specializedMutated, spv::OpLoad) == 2u, std::string(accessName) + ": mutated private table load was folded despite a store through an access chain"); - } catch (const std::exception& error) { - check(false, std::string(accessName) + " private table specialization: " + error.what()); + const auto mutated = assemble(privateTableModuleSource( + "%writeElement = " + std::string(accessName) + " %tableElement %table %zero\n" + "OpStore %writeElement %lane\n" + "%readElement = " + std::string(accessName) + " %tableElement %table %one\n" + "%loaded = OpLoad %uint %readElement\n" + "OpStore %output %loaded\n", functionTable)); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(mutated, Vulkan11, Spirv13, false, false)); + const auto specializedMutated = ShaderRecompiler::SpecializeSpirv(mutated); + static_cast(ShaderRecompiler::ValidateAndOptimizeSpirv(specializedMutated, Vulkan11, Spirv13, false, false)); + check(opcodeCount(specializedMutated, accessOp) == 2u, label + ": mutated table access chain was removed"); + check(opcodeCount(specializedMutated, spv::OpLoad) == 2u, label + ": mutated table load was folded despite a store through an access chain"); + } catch (const std::exception& error) { + check(false, label + " table specialization: " + error.what()); + } } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp index b20fa14351..a47da0f9dd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvSpecialization.cpp @@ -85,21 +85,6 @@ Instruction Make(spv::Op op, std::initializer_list operands) { return result; } -class IdSet { -public: - void Reset(std::size_t bound) { present.assign(bound, 0u); } - bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } - bool insert(std::uint32_t id) { - if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); - if (present[id] != 0u) return false; - present[id] = 1u; - return true; - } - -private: - std::vector present; -}; - bool PureInstruction(const Instruction& instruction) { switch (Opcode(instruction)) { case spv::OpVectorExtractDynamic: @@ -568,6 +553,21 @@ private: std::size_t count = 0; }; +class IdSet { +public: + void Reset(std::size_t bound) { present.assign(bound, 0u); } + bool contains(std::uint32_t id) const { return id < present.size() && present[id] != 0u; } + bool insert(std::uint32_t id) { + if (id >= present.size()) throw std::runtime_error("prepared SPIR-V id exceeds the module bound"); + if (present[id] != 0u) return false; + present[id] = 1u; + return true; + } + +private: + std::vector present; +}; + class Specialization { public: explicit Specialization(std::span words) : arena(words.size()) { @@ -599,7 +599,7 @@ public: if (op == spv::OpConstantTrue || op == spv::OpConstantFalse) values.emplace(instruction.at(2), op == spv::OpConstantTrue ? 1u : 0u); if ((op == spv::OpConstant || op == spv::OpConstantTrue || op == spv::OpConstantFalse) && values.contains(instruction.at(2))) canonical.emplace(std::pair{instruction.at(1), values.at(instruction[2])}, instruction[2]); if (op == spv::OpConstantComposite && instruction.size() >= 3u) composites.emplace(instruction.at(2), std::vector(instruction.begin() + 3, instruction.end())); - if (op == spv::OpVariable && instruction.size() == 5u && instruction[3] == spv::StorageClassPrivate && composites.contains(instruction[4])) privateTables.emplace(instruction.at(2), instruction[4]); + if (op == spv::OpVariable && instruction.size() == 5u && (instruction[3] == spv::StorageClassPrivate || instruction[3] == spv::StorageClassFunction) && composites.contains(instruction[4])) privateTables.emplace(instruction.at(2), instruction[4]); cursor += count; } IdTable tablePointers; From 65ba3a665fc60c1d932b8938746c917ad57fb036 Mon Sep 17 00:00:00 2001 From: mugurc Date: Tue, 20 Oct 2026 11:42:36 +0300 Subject: [PATCH 165/318] fix(tests): compare the floating-point exception masks with the ones the host keeps Under Rosetta 2 the six MXCSR exception masks always read back set: a write that unmasks an SSE exception is not kept, while the x87 masks and the other MXCSR bits are. guest_floating_point unmasked the overflow exception on both units and then expected MXCSR to equal the value it wrote, so it failed on macOS although fesetexceptflag and feclearexcept left the masks alone. The test now compares with the environment read back after the write, which on other hosts is the value written. The debt entry for the floating-point functions notes the Rosetta behaviour. --- core/libs/tests/GuestFloatingPoint.cpp | 1 + docs/dev/debt/libc.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index 3ea9884bb0..41275eaad7 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -142,6 +142,7 @@ void CheckFlags() { traps.x87[0] &= ~0x08u; traps.sse &= ~(0x08u << 7); WriteEnvironment(traps); + traps = ReadEnvironment(); const std::uint16_t none = 0; Require(fesetexceptflag_nid_postfix(&none, 0x01) == 0, "restore with an unmasked exception"); Require(feclearexcept_nid_postfix(0x02) == 0, "clear with an unmasked exception"); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 5014c4dd6f..12eb890d98 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -15,7 +15,7 @@ - [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep` on POSIX, retrying on `EINTR`, or high-resolution waitable timers on Windows; a null xtime or one whose normalization overflows throws. Guest thread cancellation is not integrated with this wait. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. -- [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. +- [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error From 100148f7624af4dd712837c901fd4813c43e2800 Mon Sep 17 00:00:00 2001 From: mugurc Date: Thu, 22 Oct 2026 13:00:10 +0300 Subject: [PATCH 166/318] fix(tests): expect the SSE traps the host keeps in the floating-point trap checks The trap checks unmask exceptions with feenableexcept and its relatives and expect MXCSR to hold the new masks, and the enable and disable results to report the traps enabled on both units. Under Rosetta 2 MXCSR keeps its six exception masks set, so no SSE trap is ever enabled and every result reports none. The test now asks the host once whether it keeps an unmasked MXCSR exception and, where it does not, expects all six SSE masks set and no trap on both units; on other hosts the expectations are the same as before. A debt entry records that the trap functions change only the x87 masks under Rosetta. --- core/libs/tests/GuestFloatingPoint.cpp | 29 ++++++++++++++++++-------- docs/dev/debt/libc.md | 1 + 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index 41275eaad7..31e3374567 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -188,25 +188,36 @@ unsigned SseMasks() { return (ReadEnvironment().sse >> 7) & 0x3fu; } +bool HostKeepsSseTraps() { + const unsigned saved = _mm_getcsr(); + _mm_setcsr(saved & ~(0x01u << 7)); + const bool kept = (_mm_getcsr() & (0x01u << 7)) == 0; + _mm_setcsr(saved); + return kept; +} + void CheckTraps() { const auto seeded = SeedFlags(0, 0); + const bool sseKept = HostKeepsSseTraps(); + const auto sse = [&](unsigned masks) { return sseKept ? masks : 0x3fu; }; + const auto both = [&](unsigned traps) { return static_cast(0xffffffc0u | (sseKept ? traps : 0u)); }; Require(feenableexcept_nid_postfix(0x49) == static_cast(0xffffffc0u), "enable reports no previous trap as the console does"); Require(X87Masks() == 0x36 && fegettrapenable_nid_postfix() == 0x09, "enable unmasks the x87 exceptions"); - Require(SseMasks() == 0x36, "enable unmasks the SSE exceptions"); - Require(fedisableexcept_nid_postfix(0x41) == static_cast(0xffffffc9u), "disable returns the traps enabled on both units"); + Require(SseMasks() == sse(0x36), "enable unmasks the SSE exceptions"); + Require(fedisableexcept_nid_postfix(0x41) == both(0x09), "disable returns the traps enabled on both units"); Require(X87Masks() == 0x37 && fegettrapenable_nid_postfix() == 0x08, "disable masks the x87 exception"); - Require(SseMasks() == 0x37, "disable masks the SSE exception"); - Require(__feenableexcept_nid_postfix(0x02) == static_cast(0xffffffc8u) && X87Masks() == 0x35, "the internal enable differs"); - Require(__fedisableexcept_nid_postfix(0x3f) == static_cast(0xffffffcau) && X87Masks() == 0x3f && fegettrapenable_nid_postfix() == 0, "the internal disable differs"); + Require(SseMasks() == sse(0x37), "disable masks the SSE exception"); + Require(__feenableexcept_nid_postfix(0x02) == both(0x08) && X87Masks() == 0x35, "the internal enable differs"); + Require(__fedisableexcept_nid_postfix(0x3f) == both(0x0a) && X87Masks() == 0x3f && fegettrapenable_nid_postfix() == 0, "the internal disable differs"); Require(fesettrapenable_nid_postfix(0x40) == 1 && X87Masks() == 0x3f, "an out-of-range trap set was accepted"); - Require(fesettrapenable_nid_postfix(0x10004) == 0 && fegettrapenable_nid_postfix() == 0x04 && SseMasks() == 0x3b, "a trap set did not use the low 16 bits"); - Require(fesettrapenable_nid_postfix(0x01) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "a trap set masked enabled traps"); - Require(fesettrapenable_nid_postfix(0) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == 0x3a, "an empty trap set masked enabled traps"); + Require(fesettrapenable_nid_postfix(0x10004) == 0 && fegettrapenable_nid_postfix() == 0x04 && SseMasks() == sse(0x3b), "a trap set did not use the low 16 bits"); + Require(fesettrapenable_nid_postfix(0x01) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == sse(0x3a), "a trap set masked enabled traps"); + Require(fesettrapenable_nid_postfix(0) == 0 && fegettrapenable_nid_postfix() == 0x05 && SseMasks() == sse(0x3a), "an empty trap set masked enabled traps"); auto mixed = ReadEnvironment(); mixed.x87[0] = (mixed.x87[0] & ~0x3fu) | 0x36; mixed.sse = (mixed.sse & ~(0x3fu << 7)) | (0x3au << 7); WriteEnvironment(mixed); - Require(feenableexcept_nid_postfix(0) == static_cast(0xffffffc1u), "enable reports only the traps enabled on both units"); + Require(feenableexcept_nid_postfix(0) == both(0x01), "enable reports only the traps enabled on both units"); Require(fegettrapenable_nid_postfix() == 0x09, "the trap query reads only the x87 control word"); WriteEnvironment(seeded); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 12eb890d98..8702e50803 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -16,6 +16,7 @@ - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. - [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. +- [feenableexcept, fedisableexcept, __feenableexcept, __fedisableexcept, fegettrapenable and fesettrapenable](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libc) change only the x87 masks under Rosetta 2: MXCSR keeps its six exception masks set there, so no SSE trap is enabled, and the enable and disable results, which report the traps enabled on both units, report none - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error From 2d7a8dab59037b7e12aace0eebef279ba6fa7284 Mon Sep 17 00:00:00 2001 From: ebosido444-ai Date: Thu, 22 Oct 2026 13:03:32 +0300 Subject: [PATCH 167/318] test(agc): cover a layered depth view's layer that only overlaps the depth slice The layer-pitch assertion was guarded by layerPitch + sliceBytes > BlockBytes, which holds for this fixture, so it never ran. It now asserts the content of the layer the depth surface does not start at, and a new case fills a 64 KiB swizzled depth plane without storing it back, takes a four-layer view over the same memory, and checks that the pending depth result is in guest memory before the view is uploaded and that the overlapping layer holds the guest bytes that were there. --- .../tests/execution/DepthStorage.cpp | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp index d5a4fac983..d7edece100 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DepthStorage.cpp @@ -257,7 +257,7 @@ void Run(Context context, std::uint8_t* block) { Require(layered != nullptr && layered->ImageLayers() == 2, "a two-layer 2D array view over the depth plane's guest layer pitch was not taken as a storage image"); submit(recorder); readBack(*layered); - Require(layerPitch + sliceBytes > BlockBytes || CountFloats(block + layerPitch, 0.5f) == Side * Side, "the two-layer 2D array view did not place its layers at the guest layer pitch"); + Require(CountFloats(block + layerPitch, 0.f) == Side * Side, "the two-layer 2D array view did not take the layer that only overlaps the depth slice from guest memory"); Require(CountFloats(block, 0.5f) == Side * Side, "the two-layer 2D array view did not receive the layer the depth surface holds"); layeredDepth.baseArray = 1; Require(DepthSurfaceStorage(context, {}, layeredDepth, 0) != nullptr, "a 2D array view from layer 1 of the depth plane was not taken as a storage image"); @@ -271,6 +271,24 @@ void Run(Context context, std::uint8_t* block) { submit(recorder); readBack(*reread); Require(CountFloats(block, 0.125f) == Side * Side, "a storage image written over the depth plane did not reach the depth surface"); + const float overlapping = 0.375f; + for (std::size_t i = 0; i < Side * Side; ++i) std::memcpy(block + layerPitch + i * sizeof(overlapping), &overlapping, sizeof(overlapping)); + auto swizzled = PlaneResource(address, Format32Float); + swizzled.tileMode = TextureTileMode::kStandard64KB; + const auto tiled = DepthSurfaceStorage(context, {}, swizzled, 0); + Require(tiled != nullptr, "a 64 KiB depth plane has no storage image"); + const std::array pendingFill{std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f), std::bit_cast(0.75f)}; + Require(tiled->FillClear(pendingFill, StorageTexture::WholeImage, refusal), refusal != nullptr ? refusal : "the 64 KiB depth plane's storage image could not be filled"); + submit(recorder); + auto spanning = layeredDepth; + spanning.baseArray = 0; + spanning.depthOrLastArray = 3; + const auto spanningView = DepthSurfaceStorage(context, {}, spanning, 0); + Require(spanningView != nullptr && spanningView->ImageLayers() == 4, "a four-layer 2D array view over a 64 KiB depth plane was not taken as a storage image"); + Require(CountFloats(block, 0.75f) == Side * Side, "the pending depth result was not stored into guest memory before the layered view was created"); + submit(recorder); + readBack(*spanningView); + Require(CountFloats(block + layerPitch, overlapping) == Side * Side, "the layer that only overlaps the depth surface did not hold the guest bytes that were there"); Require(!DepthSurfaceStoreBack(context, *StorageTexture::Create(context, *context.detiler, PlaneResource(address, Format32Float, Side / 2), 0)), "a storage image of another extent over the depth memory was stored into the depth surface"); } From d811e7abf025ba75c1874fb1461a330c01cf5240 Mon Sep 17 00:00:00 2001 From: hypermist Date: Fri, 23 Oct 2026 02:15:34 +1300 Subject: [PATCH 168/318] fix(libc): set strtof ERANGE as the titles' libc.prx does strtof and wcstof now set ERANGE for a decimal input whose nonzero subnormal result is inexact, never for a hexadecimal nonzero subnormal, and keep it for underflow to zero and overflow. This is what the libc.prx bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509) do when run side by side with this libc. The earlier commit followed libSceLibcInternal's _Stof, which leaves errno alone for hexadecimal under- and overflow, but titles import these NIDs from their own libc.prx. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RbLgECLQ2bpDM7oe2urGZK --- core/libs/prx/libc/src/Strings.cpp | 8 ++++++++ core/libs/tests/GuestMath.cpp | 13 +++++++++++-- core/libs/tests/GuestWideStrings.cpp | 6 +++++- docs/dev/debt/libc.md | 1 + 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index c83297b840..79a99b756e 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -195,6 +195,14 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha } if (*subject == '+' || *subject == '-') ++subject; const bool hexadecimal = subject[0] == '0' && (subject[1] == 'x' || subject[1] == 'X') && parsedEnd > subject + 2; + if constexpr (std::is_same_v) { + const float magnitude = value < 0 ? -value : value; + if (value != 0 && magnitude < std::numeric_limits::min()) { + const long double exact = std::strtold(text, nullptr); + errno = !hexadecimal && static_cast(value) != exact ? ERANGE : saved; + } + return value; + } if (!hexadecimal) return value; if constexpr (std::is_same_v) { constexpr double smallest = std::numeric_limits::denorm_min(); diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index bd3c75e4c2..aff8179bea 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -486,7 +486,6 @@ static void CheckFloatUnderflowErrno() { errno = 7; Require(strtod_nid_postfix("1e-310", &end) == 1e-310 && errno == 7); Require(strtod_nid_postfix("-4.9406564584124654e-324", &end) == -0x1p-1074 && errno == 7); - Require(strtof_nid_postfix("1e-40", &end) == 1e-40f && errno == 7); Require(strtold_nid_postfix("1e-4940", &end) == 1e-4940L && errno == 7); Require(strtod_nid_postfix("1e-330", &end) == 0.0 && errno == ERANGE); errno = 0; @@ -504,8 +503,18 @@ static void CheckFloatUnderflowErrno() { errno = 0; Require(strtod_nid_postfix("0x1p+1100", &end) == HUGE_VAL && errno == ERANGE); errno = 7; - Require(strtof_nid_postfix("0x1p-150", &end) == 0.0f && strtof_nid_postfix("0x1p+200", &end) == HUGE_VALF && errno == 7); Require(strtold_nid_postfix("0x1p-16446", &end) == 0.0L && strtold_nid_postfix("0x1p+20000", &end) == HUGE_VALL && errno == 7); + Require(strtof_nid_postfix("0x1p-149", &end) == 0x1p-149f && strtof_nid_postfix("0x1.8p-149", &end) == 0x1p-148f && strtof_nid_postfix("0x1.000001p-149", &end) == 0x1p-149f && errno == 7); + Require(strtof_nid_postfix("1.40129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158203125e-45", &end) == 0x1p-149f && strtof_nid_postfix("1.17549435e-38", &end) == 0x1p-126f && errno == 7); + Require(strtof_nid_postfix("0x1p-150", &end) == 0.0f && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("0x1p+200", &end) == HUGE_VALF && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("1e-40", &end) == 1e-40f && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("-1e-45", &end) == -0x1p-149f && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("3e-45", &end) == 0x1p-148f && errno == ERANGE); Require(strtof_nid_postfix("1e39", &end) == HUGE_VALF && errno == ERANGE); errno = 0; } diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 35c4e9cc0b..e36c75fcee 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -344,7 +344,11 @@ int main() { const char16_t* sign = u" -x"; require(std::signbit(wcstof_nid_postfix(sign, &end)) && end == sign && std::signbit(wcstold_nid_postfix(sign, &end)) && !std::signbit(wcstod_nid_postfix(sign, &end))); *__error_nid_postfix() = 0; - require(wcstof_nid_postfix(u"0x1p+200", nullptr) == HUGE_VALF && *__error_nid_postfix() == 0); + require(wcstof_nid_postfix(u"0x1p+200", nullptr) == HUGE_VALF && *__error_nid_postfix() == ERANGE); + *__error_nid_postfix() = 0; + require(wcstof_nid_postfix(u"1e-45", nullptr) == 0x1p-149f && *__error_nid_postfix() == ERANGE); + *__error_nid_postfix() = 0; + require(wcstof_nid_postfix(u"0x1p-149", nullptr) == 0x1p-149f && *__error_nid_postfix() == 0); const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 7c631d55b5..1c9275dbac 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -59,6 +59,7 @@ - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced +- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `ERANGE` follows the `libc.prx` bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509), both run side by side with this libc on a Linux host: a decimal input whose result is a nonzero subnormal sets it only when inexact, a hexadecimal one never does, and underflow to zero and overflow set it. Both builds also read a `NAN(...)` sequence in `strtof` as their `strtod` does, keep the sign and take a payload from its leading digits (`nan(0x123)` is `0x7fc00123`, `-nan(5)` is `0xffc00005`); that is not reproduced. Decimal `strtod` sets `ERANGE` for a nonzero subnormal in J03547016 but not in J03848509 or `libSceLibcInternal`, whose result is kept. - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. From 3cf419d4eab3a07a9e84f76e5f6062d224cc0259 Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Tue, 20 Oct 2026 21:04:58 +0200 Subject: [PATCH 169/318] fix(libSceRtc): handle out-of-range time values as the PS4 does Measured on a PS4 (firmware 10.71) with an OpenOrbis homebrew that loads a probe PRX calling libSceRtc; every output starts as a sentinel, so stores and their width are visible. - sceRtcSetTime_t takes the low 32 bits of its argument as unsigned seconds and returns 0 for every value: -1 is 2106-02-07 06:28:15, 2^32 + 1 is 1970-01-01 00:00:01 and -2^31 is 2038-01-19 03:14:08. sceRtcGetTime_t stays 64-bit (2106-02-07 06:28:16 is 2^32). - sceRtcGetTime_t before 1970 and sceRtcGetWin32FileTime before 1601 store 0 and return 0x80B50008. Field validation still comes first and leaves the output unchanged (1969-13-01 returns 0x80B50009). - sceRtcSetTick and sceRtcSetWin32FileTime have no upper limit: the tick after 9999-12-31 23:59:59.999999 is 10000-01-01 and the file time 2^64 - 1 is 60056-05-28 05:36:10.955161. sceRtcGetDayOfWeek accepts years past 9999 (10000-01-01 is 6). - sceRtcFormatRFC3339 returns 0x80B50008 when the zone-shifted tick is outside years 1 to 9999. sceRtcFormatRFC2822 returns 0x80B50003 when it is negative as a signed value and 0x80B50008 when it is past 9999. The PS5-measured cases of GuestRtc.cpp that the probe repeated (RFC 3339 and RFC 2822 parsing, named and military zones, DOS time, TickAdd) give the same results on the PS4. The three expectations changed here (SetTick past 9999, SetTime_t(-1), RFC 2822 at the limit with +1 minute) came from commits that cite no measurement. Not yet confirmed on PS5. Written with AI assistance. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceRtc/Export.cpp | 26 ++++++++++++---------- core/libs/tests/GuestRtc.cpp | 35 +++++++++++++++++++++++++++--- docs/dev/debt/libSceRtc.md | 1 + 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 3cc3b045de..31c561305f 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -370,7 +370,7 @@ int APS5_VABI sceRtcGetDaysInMonth(int year, int month) { } int APS5_VABI sceRtcGetDayOfWeek(int year, int month, int day) { - if (year < 1 || year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; + if (year < 1) return SCE_RTC_ERROR_INVALID_YEAR; if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; if (day < 1 || day > daysInMonth(year, month)) return SCE_RTC_ERROR_INVALID_DAY; return static_cast((daysFromCivil(year, static_cast(month), static_cast(day)) + 1) % 7); @@ -389,7 +389,6 @@ int APS5_VABI sceRtcGetTick(const RtcDateTime* time, RtcTick* tick) { int APS5_VABI sceRtcSetTick(RtcDateTime* time, const RtcTick* tick) { if (!time || !tick) return SCE_RTC_ERROR_INVALID_POINTER; - if (tick->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; *time = fromTick(tick->tick); return 0; } @@ -442,15 +441,17 @@ int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds) { if (!seconds) return SCE_RTC_ERROR_INVALID_POINTER; if (const int result = validate(time); result != 0) return result; const std::uint64_t tick = toTick(*time); - if (tick < UNIX_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + if (tick < UNIX_EPOCH_TICK) { + *seconds = 0; + return SCE_RTC_ERROR_INVALID_YEAR; + } *seconds = static_cast((tick - UNIX_EPOCH_TICK) / TICKS_PER_SECOND); return 0; } int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (seconds < 0 || static_cast(seconds) > (MAX_TICK - UNIX_EPOCH_TICK) / TICKS_PER_SECOND) return SCE_RTC_ERROR_INVALID_VALUE; - *time = fromTick(UNIX_EPOCH_TICK + static_cast(seconds) * TICKS_PER_SECOND); + *time = fromTick(UNIX_EPOCH_TICK + static_cast(static_cast(seconds)) * TICKS_PER_SECOND); return 0; } @@ -458,14 +459,16 @@ int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime* time, uint64_t* win32_ti if (!win32_time) return SCE_RTC_ERROR_INVALID_POINTER; if (const int result = validate(time); result != 0) return result; const std::uint64_t tick = toTick(*time); - if (tick < WIN32_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + if (tick < WIN32_EPOCH_TICK) { + *win32_time = 0; + return SCE_RTC_ERROR_INVALID_YEAR; + } *win32_time = (tick - WIN32_EPOCH_TICK) * 10; return 0; } int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime* time, uint64_t win32_time) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; - if (win32_time / 10 > MAX_TICK - WIN32_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; *time = fromTick(WIN32_EPOCH_TICK + win32_time / 10); return 0; } @@ -587,8 +590,8 @@ int APS5_VABI sceRtcFormatRFC3339(char* dateTime, const RtcTick* utc, int timeZo utc = &now; } if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; - RtcTick local{}; - if (const int result = addTicks(&local, utc, timeZoneMinutes, TICKS_PER_MINUTE); result != 0) return result; + const RtcTick local{utc->tick + static_cast(static_cast(timeZoneMinutes) * TICKS_PER_MINUTE)}; + if (local.tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_YEAR; const RtcDateTime time = fromTick(local.tick); int written = std::snprintf(dateTime, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); if (timeZoneMinutes == 0) { @@ -619,8 +622,9 @@ int APS5_VABI sceRtcFormatRFC2822(char* dateTime, const RtcTick* utc, int timeZo utc = &now; } if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; - RtcTick local{}; - if (const int result = addTicks(&local, utc, timeZoneMinutes, TICKS_PER_MINUTE); result != 0) return result; + const RtcTick local{utc->tick + static_cast(static_cast(timeZoneMinutes) * TICKS_PER_MINUTE)}; + if (static_cast(local.tick) < 0) return SCE_RTC_ERROR_INVALID_VALUE; + if (local.tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_YEAR; const RtcDateTime time = fromTick(local.tick); const int offset = timeZoneMinutes < 0 ? -timeZoneMinutes : timeZoneMinutes; std::snprintf(dateTime, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(local.tick / TICKS_PER_DAY + 1) % 7], time.day, diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 53e55fd327..36c8411c72 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -134,6 +134,7 @@ int main() { Require(sceRtcGetDaysInMonth(10000, 1) == 31); Require(sceRtcGetDaysInMonth(9999, 1) == 31); Require(sceRtcGetDayOfWeek(1, 1, 1) == 1 && sceRtcGetDayOfWeek(2026, 9, 26) == 6); + Require(sceRtcGetDayOfWeek(10000, 1, 1) == 6); Require(sceRtcGetDayOfWeek(2023, 2, 29) == invalidDay); RtcDateTime leapDay{2024, 2, 29, 12, 34, 56, 789000}; @@ -156,7 +157,7 @@ int main() { tick.tick = maxTick; Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, RtcDateTime{9999, 12, 31, 23, 59, 59, 999999})); tick.tick = maxTick + 1; - Require(sceRtcSetTick(&converted, &tick) == invalidValue); + Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, RtcDateTime{10000, 1, 1, 0, 0, 0, 0})); Require(sceRtcGetCurrentClock(nullptr, 0) == invalidPointer); Require(sceRtcGetCurrentClock(&converted, 0) == 0 && sceRtcCheckValid(&converted) == 0); @@ -171,10 +172,24 @@ int main() { RtcDateTime millennium{2000, 1, 1, 0, 0, 0, 0}; Require(sceRtcGetTime_t(&millennium, &seconds) == 0 && seconds == 946684800); Require(sceRtcSetTime_t(&converted, 946684800) == 0 && Equal(converted, millennium)); - Require(sceRtcSetTime_t(&converted, -1) == invalidValue); + Require(sceRtcSetTime_t(&converted, -1) == 0 && Equal(converted, RtcDateTime{2106, 2, 7, 6, 28, 15, 0})); + Require(sceRtcSetTime_t(&converted, 0x100000001ll) == 0 && Equal(converted, RtcDateTime{1970, 1, 1, 0, 0, 1, 0})); + Require(sceRtcSetTime_t(&converted, -0x80000000ll) == 0 && Equal(converted, RtcDateTime{2038, 1, 19, 3, 14, 8, 0})); + const RtcDateTime past2106{2106, 2, 7, 6, 28, 16, 0}; + Require(sceRtcGetTime_t(&past2106, &seconds) == 0 && seconds == 0x100000000ll); + const RtcDateTime beforeEpoch{1969, 12, 31, 23, 59, 59, 0}; + seconds = -1; + Require(sceRtcGetTime_t(&beforeEpoch, &seconds) == invalidYear && seconds == 0); + const RtcDateTime badMonth{1969, 13, 1, 0, 0, 0, 0}; + seconds = -1; + Require(sceRtcGetTime_t(&badMonth, &seconds) == invalidMonth && seconds == -1); std::uint64_t fileTime = 0; Require(sceRtcGetWin32FileTime(&epoch, &fileTime) == 0 && fileTime == 116444736000000000ull); Require(sceRtcSetWin32FileTime(&converted, 116444736000000000ull) == 0 && Equal(converted, epoch)); + const RtcDateTime before1601{1600, 12, 31, 23, 59, 59, 0}; + fileTime = 1; + Require(sceRtcGetWin32FileTime(&before1601, &fileTime) == invalidYear && fileTime == 0); + Require(sceRtcSetWin32FileTime(&converted, std::numeric_limits::max()) == 0 && Equal(converted, RtcDateTime{60056, 5, 28, 5, 36, 10, 955161})); std::uint32_t dosTime = 0xffffffffu; const RtcDateTime dosDate{2024, 2, 29, 12, 34, 57, 789000}; Require(sceRtcGetDosTime(&dosDate, &dosTime) == 0 && dosTime == 0x585d645cu); @@ -270,6 +285,16 @@ int main() { Require(sceRtcFormatRFC3339(text, &tick, offset) == invalidValue); Require(std::memcmp(text, original, sizeof(text)) == 0); } + RtcTick edge{maxTick + 1}; + Require(sceRtcFormatRFC3339(text, &edge, 0) == invalidYear); + edge.tick = std::numeric_limits::max(); + Require(sceRtcFormatRFC3339(text, &edge, 0) == invalidYear); + edge.tick = maxTick; + Require(sceRtcFormatRFC3339(text, &edge, 1) == invalidYear); + edge.tick = 59999999; + Require(sceRtcFormatRFC3339(text, &edge, -1) == invalidYear); + edge.tick = 60000000; + Require(sceRtcFormatRFC3339(text, &edge, -1) == 0 && std::strcmp(text, "0001-01-01T00:00:00.00-00:01") == 0); Require(sceRtcFormatRFC3339(nullptr, &tick, 1440) == invalidPointer); Require(sceRtcFormatRFC3339(text, nullptr, 1440) == invalidValue); { @@ -775,7 +800,11 @@ int main() { Require(sceRtcFormatRFC2822(rfc2822, &tick, -1) == invalidValue); tick.tick = maxTick; Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0 && std::strcmp(rfc2822, "Fri, 31 Dec 9999 23:59:59 +0000") == 0); - Require(sceRtcFormatRFC2822(rfc2822, &tick, 1) == invalidValue); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1) == invalidYear); + tick.tick = maxTick + 1; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == invalidYear); + tick.tick = std::numeric_limits::max(); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == invalidValue); tick.tick = leapDayTick; for (int offset : {1440, -1440, std::numeric_limits::min(), std::numeric_limits::max()}) { std::memset(rfc2822, 'x', sizeof(rfc2822)); diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 56985f25a2..0139c6860b 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -7,6 +7,7 @@ ### Unknown function info - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. +- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_VALUE` when the zone-shifted tick is negative as a signed value, `INVALID_YEAR` when it is past 9999, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick returning `INVALID_POINTER` (shadPS4 formats the current time instead) and offsets outside ±1439 minutes returning `INVALID_VALUE` are not measured for these three functions - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console From 2d1ecaf29e3b4d9038925c020edd3de39d65fa40 Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Tue, 20 Oct 2026 21:22:23 +0200 Subject: [PATCH 170/318] fix(libSceRtc): reject a negative source tick in sceRtcFormatRFC2822 Measured on a PS4 (firmware 10.71) for the review of the zone-shift overflow: sceRtcFormatRFC2822 returns 0x80B50003 for UINT64_MAX with +1 and +1439 minutes and for 2^64 - 1 minute with +1, where the shifted tick lands in year 1. The console returns 0x80B50008 when the shifted tick is past 9999 as a signed value (2^63 with -1 minute) and otherwise 0x80B50003 when the source or the shifted tick is negative as a signed value (2^63 with 0, 0 with -1); the tick after 9999 with -1 minute still formats. sceRtcFormatRFC3339 does wrap on the console: UINT64_MAX with +1 minute returns 0 and writes 0001-01-01T00:00:59.99+00:01, as the implementation already does. Tests now cover both. Written with AI assistance. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceRtc/Export.cpp | 4 ++-- core/libs/tests/GuestRtc.cpp | 14 ++++++++++++++ docs/dev/debt/libSceRtc.md | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 31c561305f..a1dffea9ac 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -623,8 +623,8 @@ int APS5_VABI sceRtcFormatRFC2822(char* dateTime, const RtcTick* utc, int timeZo } if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; const RtcTick local{utc->tick + static_cast(static_cast(timeZoneMinutes) * TICKS_PER_MINUTE)}; - if (static_cast(local.tick) < 0) return SCE_RTC_ERROR_INVALID_VALUE; - if (local.tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_YEAR; + if (static_cast(local.tick) > static_cast(MAX_TICK)) return SCE_RTC_ERROR_INVALID_YEAR; + if (static_cast(utc->tick) < 0 || static_cast(local.tick) < 0) return SCE_RTC_ERROR_INVALID_VALUE; const RtcDateTime time = fromTick(local.tick); const int offset = timeZoneMinutes < 0 ? -timeZoneMinutes : timeZoneMinutes; std::snprintf(dateTime, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(local.tick / TICKS_PER_DAY + 1) % 7], time.day, diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 36c8411c72..20a921e854 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -295,6 +295,12 @@ int main() { Require(sceRtcFormatRFC3339(text, &edge, -1) == invalidYear); edge.tick = 60000000; Require(sceRtcFormatRFC3339(text, &edge, -1) == 0 && std::strcmp(text, "0001-01-01T00:00:00.00-00:01") == 0); + edge.tick = std::numeric_limits::max(); + Require(sceRtcFormatRFC3339(text, &edge, 1) == 0 && std::strcmp(text, "0001-01-01T00:00:59.99+00:01") == 0); + edge.tick = 1ull << 63; + Require(sceRtcFormatRFC3339(text, &edge, -1) == invalidYear); + edge.tick = maxTick + 1; + Require(sceRtcFormatRFC3339(text, &edge, -1) == 0 && std::strcmp(text, "9999-12-31T23:59:00.00-00:01") == 0); Require(sceRtcFormatRFC3339(nullptr, &tick, 1440) == invalidPointer); Require(sceRtcFormatRFC3339(text, nullptr, 1440) == invalidValue); { @@ -805,6 +811,14 @@ int main() { Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == invalidYear); tick.tick = std::numeric_limits::max(); Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == invalidValue); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1) == invalidValue); + tick.tick = 0ull - 60000000ull; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1) == invalidValue); + tick.tick = 1ull << 63; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == invalidValue); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -1) == invalidYear); + tick.tick = maxTick + 1; + Require(sceRtcFormatRFC2822(rfc2822, &tick, -1) == 0 && std::strcmp(rfc2822, "Fri, 31 Dec 9999 23:59:00 -0001") == 0); tick.tick = leapDayTick; for (int offset : {1440, -1440, std::numeric_limits::min(), std::numeric_limits::max()}) { std::memset(rfc2822, 'x', sizeof(rfc2822)); diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index 0139c6860b..affc0d5d23 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -7,7 +7,7 @@ ### Unknown function info - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. -- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_VALUE` when the zone-shifted tick is negative as a signed value, `INVALID_YEAR` when it is past 9999, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick returning `INVALID_POINTER` (shadPS4 formats the current time instead) and offsets outside ±1439 minutes returning `INVALID_VALUE` are not measured for these three functions +- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_YEAR` when the zone-shifted tick is past 9999 as a signed value, otherwise `INVALID_VALUE` when the source tick or the zone-shifted tick is negative as a signed value, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick formatting the current time and the ±1439 minute offset limit come from the PS5 measurement in the next entry and are not measured on the PS4 - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console From 0edbb0d2fbae72e1a615b9dc0a30d84670554c07 Mon Sep 17 00:00:00 2001 From: m335671 <110462702+m335671@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:28:13 +0200 Subject: [PATCH 171/318] refactor(agc): split a dispatch's device call into a complete and a record step VulkanDevice::dispatch now ends after the resources are complete and the pipeline is resolved, and hands a DispatchCompletion (references to the context, shader, push constants, pipeline objects, resources and the cache outcome) to recordCompletedDispatch, which records the dispatch and runs what follows it: the lease sync, the traces and the recipe. No behaviour changes; the code of both halves is unchanged. This is the dispatch-side counterpart of the Graphics::Draw prepare and record split discussed in #7895, so that later steps can complete a dispatch apart from recording it. Co-Authored-By: Claude Opus 5.5 --- .../Execution/include/VulkanDevice.hpp | 2 + .../Execution/src/VulkanDevice.cpp | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 52303ede78..252ec943c9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -32,6 +32,7 @@ struct ComputePipelineObjects; struct RecipeHit; // The record tail shared by an ordinary dispatch and a recipe hit (VulkanDevice::recordDispatch). struct RecordedDispatch; +struct DispatchCompletion; // The module, layout and pipeline a compute dispatch binds (VulkanDevice::createComputePipeline). struct ComputePipelineObjects; @@ -359,6 +360,7 @@ private: Graphics::Context buildContext() const; // Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups. IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span snapshots, std::uint64_t programAddress, std::shared_ptr prepared, std::shared_ptr* recipe, const std::array& threadGroup = {}, std::optional> htileClear = std::nullopt); + IndirectOutcome recordCompletedDispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, std::shared_ptr* recipeOut, const std::array& threadGroup, std::optional> htileClear, DispatchCompletion& completion); // The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none. std::uint64_t presync(std::span surfaces); std::shared_ptr computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 55e54a39bb..a15542c847 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4423,6 +4423,24 @@ struct RecordedDispatch { std::array threadLimit{}; }; +struct DispatchCompletion { + const Graphics::Context& context; + const std::array& shaders; + VkShaderStageFlags pushStages; + const std::array& pushBytes; + const std::shared_ptr& objects; + const std::shared_ptr& resources; + const ResourceCache::Key& contentKey; + bool cacheable; + bool fromCache; + double revalidateMs; + const std::shared_ptr& overlay; + std::uint64_t lookupsBefore; + DispatchTimer& timer; + PerformanceTimer& timing; + char* groupsText; +}; + VkDevice VulkanDevice::Device() const { return state->device; } @@ -5338,6 +5356,27 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec timing.Mark("pipeline_create"); timer.phase(PhasePipeline); } + DispatchCompletion completion{context, shaders, pushStages, pushBytes, objects, resources, contentKey, cacheable, fromCache, revalidateMs, overlay, lookupsBefore, timer, timing, groupsText}; + return recordCompletedDispatch(shader, x, y, z, arguments, programAddress, recipeOut, threadGroup, htileClear, completion); +} + +VulkanDevice::IndirectOutcome VulkanDevice::recordCompletedDispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, std::shared_ptr* recipeOut, const std::array& threadGroup, std::optional> htileClear, DispatchCompletion& completion) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + const auto& context = completion.context; + const auto& shaders = completion.shaders; + const auto pushStages = completion.pushStages; + const auto& pushBytes = completion.pushBytes; + const auto& objects = completion.objects; + const auto& resources = completion.resources; + const auto& contentKey = completion.contentKey; + const bool cacheable = completion.cacheable; + const bool fromCache = completion.fromCache; + const double revalidateMs = completion.revalidateMs; + const auto& overlay = completion.overlay; + const auto lookupsBefore = completion.lookupsBefore; + auto& timer = completion.timer; + auto& timing = completion.timing; + auto* groupsText = completion.groupsText; // Recorded into the device's open batch: the CPU moves on to the next command while the GPU // works; the batch is waited for where the guest expects results (labels, flips, CPU reads). auto& recorder = *state->recorder; From 795c3903392405b49849bcd90eccb2a6d6cb2380 Mon Sep 17 00:00:00 2001 From: Pacuka <69674426+Pacuka@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:11:12 +0200 Subject: [PATCH 172/318] fix(libSceAgc): take the semaphore address second in Acb/Dcb MemSemaphore PPSA03396 calls sceAgcAcbMemSemaphore and sceAgcDcbMemSemaphore as (cb, address, waitForMailbox, signalType, operation). Both read the address from the fourth argument instead, so the call aborted with "null or misaligned address 0x1". Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgc/Acb/src/Sync.cpp | 5 +-- core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp | 5 +-- core/libs/prx/libSceAgc/tests/Semaphore.cpp | 38 ++++++++++++++++++++ docs/dev/debt/libSceAgc.md | 1 + 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceAgc/Acb/src/Sync.cpp b/core/libs/prx/libSceAgc/Acb/src/Sync.cpp index eee05b37c4..cb4ff7f5a6 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Sync.cpp @@ -44,10 +44,7 @@ std::uint32_t APS5_VABI sceAgcAcbQueueEndOfShaderActionGetSize() { return 32; } -std::uint32_t* APS5_VABI sceAgcAcbMemSemaphore(CommandBuffer* buf, std::uint8_t action, std::uint8_t clientCode, const volatile void* address) { - const std::uint8_t operation = action != 0 ? 7u : 6u; - const std::uint8_t signalType = clientCode & 1u; - const std::uint8_t waitForMailbox = (clientCode >> 1u) & 1u; +std::uint32_t* APS5_VABI sceAgcAcbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint8_t waitForMailbox, std::uint8_t signalType, std::uint8_t operation) { return Agc::Command::WriteMemSemaphore(buf, address, waitForMailbox, signalType, operation, __func__); } diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp index 96c45f0d3c..d3fb4b534b 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Sync.cpp @@ -52,10 +52,7 @@ std::uint32_t APS5_VABI sceAgcDcbStallCommandBufferParserGetSize() { return 8; } -std::uint32_t* APS5_VABI sceAgcDcbMemSemaphore(CommandBuffer* buf, std::uint8_t action, std::uint8_t clientCode, const volatile void* address) { - const std::uint8_t operation = action != 0 ? 7u : 6u; - const std::uint8_t signalType = clientCode & 1u; - const std::uint8_t waitForMailbox = (clientCode >> 1u) & 1u; +std::uint32_t* APS5_VABI sceAgcDcbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint8_t waitForMailbox, std::uint8_t signalType, std::uint8_t operation) { return Agc::Command::WriteMemSemaphore(buf, address, waitForMailbox, signalType, operation, __func__); } diff --git a/core/libs/prx/libSceAgc/tests/Semaphore.cpp b/core/libs/prx/libSceAgc/tests/Semaphore.cpp index 789a303ad8..362d180ad6 100644 --- a/core/libs/prx/libSceAgc/tests/Semaphore.cpp +++ b/core/libs/prx/libSceAgc/tests/Semaphore.cpp @@ -15,6 +15,8 @@ extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes); extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label); extern "C" std::uint32_t* APS5_VABI sceAgcCbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint32_t operation, std::uint32_t modifier, std::uint32_t value); +extern "C" std::uint32_t* APS5_VABI sceAgcAcbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint8_t waitForMailbox, std::uint8_t signalType, std::uint8_t operation); +extern "C" std::uint32_t* APS5_VABI sceAgcDcbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint8_t waitForMailbox, std::uint8_t signalType, std::uint8_t operation); namespace { @@ -180,6 +182,40 @@ void testMemSemaphore() { expectFailure([&] { sceAgcCbMemSemaphore(nullptr, label, 0, 0, 0); }); } +using QueueMemSemaphore = std::uint32_t* APS5_VABI (*)(CommandBuffer*, const volatile void*, std::uint8_t, std::uint8_t, std::uint8_t); + +void testQueueMemSemaphore(QueueMemSemaphore write) { + const auto* semaphore = reinterpret_cast(0x102f221d98ull); + + { + std::array words{}; + words.fill(0xdeadbeefu); + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = write(&buffer, semaphore, 0, 1, 6); + check(packet == words.data(), "packet address mismatch"); + check(packet[0] == 0xc0023900u, "MEM_SEMAPHORE header opcode mismatch"); + check(packet[1] == 0x2f221d98u, "MEM_SEMAPHORE address low mismatch"); + check(packet[2] == 0x10u, "MEM_SEMAPHORE address high mismatch"); + check(packet[3] == 0xc0100000u, "MEM_SEMAPHORE signal control word mismatch"); + check(buffer.cursor_up == words.data() + 4, "cursor did not advance by 4 words"); + check(words[4] == 0xdeadbeefu, "words past packet modified"); + } + + { + std::array words{}; + words.fill(0xdeadbeefu); + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = write(&buffer, semaphore, 1, 0, 7); + check(packet[3] == 0xe0010000u, "MEM_SEMAPHORE wait control word mismatch"); + } + + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + expectFailure([&] { write(&buffer, reinterpret_cast(1ull), 0, 1, 6); }); + expectFailure([&] { write(&buffer, semaphore, 0, 1, 5); }); + expectFailure([&] { write(&buffer, semaphore, 2, 0, 7); }); +} + } int main() { @@ -191,6 +227,8 @@ int main() { testRegistration(); testLabels(); testMemSemaphore(); + testQueueMemSemaphore(sceAgcAcbMemSemaphore); + testQueueMemSemaphore(sceAgcDcbMemSemaphore); testLabelRejections(); testRepeatedRegistration(); storage.reset(); diff --git a/docs/dev/debt/libSceAgc.md b/docs/dev/debt/libSceAgc.md index ab5eaacf4e..cb177fed98 100644 --- a/docs/dev/debt/libSceAgc.md +++ b/docs/dev/debt/libSceAgc.md @@ -24,6 +24,7 @@ - [sceAgcCreateInterpolantMapping_10000](../../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - name from its NID (`dbOlWdppb4o`); same arguments as `sceAgcCreateInterpolantMapping_0100`; PPSA26344 passes three pointers at each of nine observed call sites (`0x9F914E`, `0x9F91E2`, `0x9F9216`, `0x1604BBD`, `0x16122D7`, `0x1618285`, `0x161838C`, `0x16184EE`, `0x1618607`), consistent with that argument assumption, but does not read the return value there, so its return type is not established. The `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware - [sceAgcGetDataPacketPayloadRange](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `sceAgcGetDataPacketPayloadAddress_0090`; prosper returns header + 2 for both types from that address function - [sceAgcGetDataPacketPayloadAddress](../../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - forward to `sceAgcGetDataPacketPayloadAddress_0090` and the inferred (addr, cmd, type) parameters follow SharpProspero and decrypted firmware 3.20 NID mapping; not verified on console hardware +- [sceAgcAcbMemSemaphore, sceAgcDcbMemSemaphore](../../../core/libs/prx/libSceAgc/Acb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, waitForMailbox, signalType, operation) inferred from PPSA03396, whose wrapper passes the address second and operation 6 (signal) or 7 (wait) last; the three trailing arguments go to the packet as-is - [sceAgcCbMemSemaphore](../../../core/libs/prx/libSceAgc/Cb/src/Sync.cpp) (libSceAgc) - signature (cb, semaphoreAddr, operation, modifier, value) follows SharpProspero; writes 4-dword PM4 0x39 packet with control word 0x80100000 following console oracle measurements in prosper (cases agcbuild_CbMemSemaphore_a/b/c); operation, modifier and value are unused - [sceAgcAsyncCondExecPatchSetCommandAddress, sceAgcAsyncCondExecPatchSetEnd](../../../core/libs/prx/libSceAgc/Patch/src/CondExec.cpp) (libSceAgc) - no reference has a body for them; they are assumed to patch the COND_EXEC packet like `sceAgcCondExecPatchSetCommandAddress` and `sceAgcCondExecPatchSetEnd`, since `sceAgcAcbCondExec` writes the same packet as `sceAgcDcbCondExec`, as `sceAgcAsyncRewindPatchSetRewindState` does for REWIND - [sceAgcAcbAtomicGds_0900](../../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - name from its NID (`gQkqkLttcpw`), signature unknown From 414ed51714957d137086fccf19d6f2e55f94be82 Mon Sep 17 00:00:00 2001 From: "T. Vuokko" Date: Thu, 22 Oct 2026 18:30:33 +0300 Subject: [PATCH 173/318] fix(relinker): skip absolute function symbols in code analysis --- .../src/analysis/CodeInstructionCollector.cpp | 4 ++- .../tests/test_guest_absolute_symbols.py | 28 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index 645dd46f3a..2e7d216a7f 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -272,7 +273,8 @@ std::set CodeInstructionCollector::Collect(const std::ve symbols = table(6, 0x61000039, symbolCount * 24); for (std::uint64_t index = 0; index < symbolCount; ++index) { const auto offset = symbols + index * 24; - if ((bytes[offset + 4] & 15) == 2 && Io::ReadU16(bytes, offset + 6) != 0) { + const auto section = Io::ReadU16(bytes, offset + 6); + if ((bytes[offset + 4] & 15) == 2 && section != 0 && section != Elfpatcher::SHN_ABS) { const auto address = Io::ReadU64(bytes, offset + 8); addRoot(address); addFunction(address, Io::ReadU64(bytes, offset + 16), true); diff --git a/core/relinker/relinker/tests/test_guest_absolute_symbols.py b/core/relinker/relinker/tests/test_guest_absolute_symbols.py index a0c0056e57..77a57f1fba 100644 --- a/core/relinker/relinker/tests/test_guest_absolute_symbols.py +++ b/core/relinker/relinker/tests/test_guest_absolute_symbols.py @@ -12,13 +12,14 @@ from test_macos_output import MachO ABSOLUTE = 0xFFF1 COMMON = 0xFFF2 VALUE = 0x1234 +SIZE = 0x10 ADDEND = 0x10 TARGET = 0x2320 -def module_with_section(section): +def module_with_section(section, size=0): image = module_with_symbol(True) - struct.pack_into(" Date: Wed, 21 Oct 2026 19:58:25 -0500 Subject: [PATCH 174/318] fix(png): mask low-bit grayscale transparency keys --- core/Decoder/Png/src/Png.cpp | 33 +++++++ core/Decoder/Png/tests/Png.cpp | 172 +++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) diff --git a/core/Decoder/Png/src/Png.cpp b/core/Decoder/Png/src/Png.cpp index 434a589d6c..8fb90270e9 100644 --- a/core/Decoder/Png/src/Png.cpp +++ b/core/Decoder/Png/src/Png.cpp @@ -48,6 +48,37 @@ bool isChunkType(const std::uint8_t* bytes, const char* type) { return std::equal(bytes, bytes + 4, type); } +std::optional readGrayscaleTransparencyKey(std::span png) { + std::size_t offset = SIGNATURE.size(); + if (png.size() < offset || !std::equal(SIGNATURE.begin(), SIGNATURE.end(), png.begin())) return std::nullopt; + std::optional key; + while (png.size() - offset >= CHUNK_OVERHEAD) { + const std::uint32_t length = readBigEndian32(&png[offset]); + const std::uint8_t* type = &png[offset + 4]; + if (length > png.size() - offset - CHUNK_OVERHEAD) return std::nullopt; + if (isChunkType(type, "tRNS")) { + if (length != 2) return std::nullopt; + key = static_cast(png[offset + 8] << 8 | png[offset + 9]); + } + if (isChunkType(type, "IDAT") || isChunkType(type, "IEND")) return key; + offset += CHUNK_OVERHEAD + length; + } + return std::nullopt; +} + +void repairGrayscaleTransparency(const Header& header, std::span png, Image& image) { + if (header.colorType != ColorType::Grayscale || !header.hasTransparency) return; + if (header.bitDepth != 1 && header.bitDepth != 2 && header.bitDepth != 4) return; + const std::optional key = readGrayscaleTransparencyKey(png); + if (!key.has_value()) return; + const std::uint16_t mask = static_cast((1u << header.bitDepth) - 1); + if ((*key & 0xFF) == (*key & mask)) return; + const std::uint8_t transparent = static_cast((*key & mask) * (255u / mask)); + for (std::size_t offset = 3; offset < image.pixels.size(); offset += 4) { + image.pixels[offset] = static_cast(image.pixels[offset - 3] == transparent ? 0 : 255); + } +} + bool isValidFormat(std::uint8_t bitDepth, std::uint8_t colorType) { switch (colorType) { case 0: @@ -158,6 +189,8 @@ std::optional Decode(std::span png) { Image image{static_cast(width), static_cast(height), {}}; image.pixels.assign(decoded.get(), decoded.get() + static_cast(image.width) * image.height * 4); + const std::optional

header = ParseHeader(png); + if (header.has_value()) repairGrayscaleTransparency(*header, png, image); return image; } diff --git a/core/Decoder/Png/tests/Png.cpp b/core/Decoder/Png/tests/Png.cpp index 0c1ac81d1e..fbd18d353f 100644 --- a/core/Decoder/Png/tests/Png.cpp +++ b/core/Decoder/Png/tests/Png.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -50,6 +51,17 @@ static const std::uint8_t GRAY1[] = { 0x42, 0x60, 0x82, }; +static const std::uint8_t GRAY1_TRNS[] = { + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x59, 0x42, + 0x27, 0x00, 0x00, 0x00, 0x02, 0x74, 0x52, 0x4E, 0x53, 0x00, 0x02, 0x98, 0x9D, 0xAC, 0x14, 0x00, + 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA, 0x63, 0x70, 0x00, 0x00, 0x00, 0x42, 0x00, + 0x41, 0x84, 0xBF, 0x8E, 0x62, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, + 0x82, +}; + +static const std::uint8_t PNG_SIGNATURE[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; + static void AppendBigEndian32(std::vector& bytes, std::uint32_t value) { for (int shift = 24; shift >= 0; shift -= 8) bytes.push_back(static_cast(value >> shift)); } @@ -64,6 +76,130 @@ static std::vector HeaderOnly(std::uint32_t width, std::uint32_t h return bytes; } +static std::uint32_t Crc32(std::span bytes) { + std::uint32_t crc = 0xFFFFFFFFu; + for (const std::uint8_t byte : bytes) { + crc ^= byte; + for (int bit = 0; bit < 8; ++bit) crc = (crc >> 1) ^ ((crc & 1) != 0 ? 0xEDB88320u : 0u); + } + return ~crc; +} + +static std::uint32_t Adler32(std::span bytes) { + std::uint32_t low = 1; + std::uint32_t high = 0; + for (const std::uint8_t byte : bytes) { + low = (low + byte) % 65521; + high = (high + low) % 65521; + } + return high << 16 | low; +} + +static void AppendTestChunk(std::vector& png, const char* type, std::span data) { + AppendBigEndian32(png, static_cast(data.size())); + const std::size_t start = png.size(); + png.insert(png.end(), type, type + 4); + png.insert(png.end(), data.begin(), data.end()); + AppendBigEndian32(png, Crc32(std::span(png.data() + start, png.size() - start))); +} + +static std::vector ZlibStore(std::span bytes) { + Require(bytes.size() <= 0xFFFF); + const std::uint16_t length = static_cast(bytes.size()); + const std::uint16_t inverse = static_cast(~length); + std::vector output{0x78, 0x01, 0x01, static_cast(length), static_cast(length >> 8), + static_cast(inverse), static_cast(inverse >> 8)}; + output.insert(output.end(), bytes.begin(), bytes.end()); + AppendBigEndian32(output, Adler32(bytes)); + return output; +} + +static std::vector GrayLowBitPng(std::uint8_t bitDepth, std::span samples, + std::optional transparency, + std::optional precedingTransparency = std::nullopt) { + Require(bitDepth == 1 || bitDepth == 2 || bitDepth == 4); + std::vector rows(1 + (samples.size() * bitDepth + 7) / 8, 0); + std::size_t bit = 0; + for (const std::uint8_t sample : samples) { + Require(sample < (1u << bitDepth)); + rows[1 + bit / 8] |= static_cast(sample << (8 - bitDepth - bit % 8)); + bit += bitDepth; + } + + std::vector ihdr; + AppendBigEndian32(ihdr, static_cast(samples.size())); + AppendBigEndian32(ihdr, 1); + ihdr.insert(ihdr.end(), {bitDepth, 0, 0, 0, 0}); + + std::vector png(PNG_SIGNATURE, PNG_SIGNATURE + sizeof(PNG_SIGNATURE)); + AppendTestChunk(png, "IHDR", ihdr); + if (precedingTransparency.has_value()) { + const std::uint8_t key[2] = {static_cast(*precedingTransparency >> 8), static_cast(*precedingTransparency)}; + AppendTestChunk(png, "tRNS", key); + } + if (transparency.has_value()) { + const std::uint8_t key[2] = {static_cast(*transparency >> 8), static_cast(*transparency)}; + AppendTestChunk(png, "tRNS", key); + } + AppendTestChunk(png, "IDAT", ZlibStore(rows)); + AppendTestChunk(png, "IEND", std::span()); + return png; +} + +static std::uint8_t GrayScale(std::uint8_t bitDepth) { + if (bitDepth == 1) return 255; + if (bitDepth == 2) return 85; + return 17; +} + +static void RequireGrayTransparency(std::uint8_t bitDepth, std::span samples, std::uint16_t key, + std::uint8_t transparentSample, std::optional precedingKey = std::nullopt) { + const std::vector png = GrayLowBitPng(bitDepth, samples, key, precedingKey); + const std::vector original = png; + const auto header = Decoder::Png::ParseHeader(png); + Require(header.has_value()); + Require(header->width == static_cast(samples.size()) && header->height == 1 && header->bitDepth == bitDepth); + Require(header->colorType == Decoder::Png::ColorType::Grayscale && header->hasTransparency); + + const auto image = Decoder::Png::Decode(png); + Require(image.has_value() && image->pixels.size() == samples.size() * 4); + const std::uint8_t scale = GrayScale(bitDepth); + for (std::size_t i = 0; i < samples.size(); ++i) { + const std::uint8_t gray = static_cast(samples[i] * scale); + Require(image->pixels[i * 4] == gray && image->pixels[i * 4 + 1] == gray && image->pixels[i * 4 + 2] == gray); + Require(image->pixels[i * 4 + 3] == (samples[i] == transparentSample ? 0 : 255)); + } + Require(png == original); +} + +static void RequireGrayOpaque(std::uint8_t bitDepth, std::span samples) { + const std::vector png = GrayLowBitPng(bitDepth, samples, std::nullopt); + const std::vector original = png; + const auto header = Decoder::Png::ParseHeader(png); + Require(header.has_value() && !header->hasTransparency); + const auto image = Decoder::Png::Decode(png); + Require(image.has_value() && image->pixels.size() == samples.size() * 4); + const std::uint8_t scale = GrayScale(bitDepth); + for (std::size_t i = 0; i < samples.size(); ++i) { + const std::uint8_t gray = static_cast(samples[i] * scale); + Require(image->pixels[i * 4] == gray && image->pixels[i * 4 + 1] == gray && image->pixels[i * 4 + 2] == gray); + Require(image->pixels[i * 4 + 3] == 255); + } + Require(png == original); +} + +static void RequireTruncatedTransparency() { + const std::uint8_t samples[] = {0, 1}; + const std::vector png = GrayLowBitPng(1, samples, 0x0002); + const std::size_t transparencyOffset = sizeof(PNG_SIGNATURE) + 25; + const std::vector truncated(png.begin(), png.begin() + transparencyOffset + 9); + const std::vector original = truncated; + const auto header = Decoder::Png::ParseHeader(truncated); + Require(header.has_value() && !header->hasTransparency); + (void)Decoder::Png::Decode(truncated); + Require(truncated == original); +} + static void RequireRoundTrip(std::uint32_t channels, Decoder::Png::ColorType colorType) { constexpr std::uint32_t width = 20; constexpr std::uint32_t height = 10; @@ -176,6 +312,42 @@ int main() { const std::uint8_t levels[8] = {0, 255, 0, 255, 255, 0, 0, 255}; for (int i = 0; i < 8; ++i) Require(gray1->pixels[i * 4] == levels[i] && gray1->pixels[i * 4 + 3] == 255); + const auto gray1TrnsHeader = Decoder::Png::ParseHeader(GRAY1_TRNS); + Require(gray1TrnsHeader.has_value()); + Require(gray1TrnsHeader->width == 2 && gray1TrnsHeader->height == 1 && gray1TrnsHeader->bitDepth == 1); + Require(gray1TrnsHeader->colorType == Decoder::Png::ColorType::Grayscale && gray1TrnsHeader->hasTransparency); + const auto gray1Trns = Decoder::Png::Decode(GRAY1_TRNS); + Require(gray1Trns.has_value()); + Require(gray1Trns->pixels == std::vector({0, 0, 0, 0, 255, 255, 255, 255})); + + const std::uint8_t gray1Samples[] = {0, 1, 0, 1, 1, 0, 0, 1}; + RequireGrayTransparency(1, gray1Samples, 0x0000, 0); + RequireGrayTransparency(1, gray1Samples, 0x0002, 0); + RequireGrayTransparency(1, gray1Samples, 0x0100, 0); + RequireGrayTransparency(1, gray1Samples, 0xFF02, 0); + RequireGrayTransparency(1, gray1Samples, 0xFFFF, 1); + RequireGrayTransparency(1, gray1Samples, 0x0001, 1, 0x0002); + RequireGrayOpaque(1, gray1Samples); + + const std::uint8_t gray2Samples[] = {0, 1, 2, 3}; + RequireGrayTransparency(2, gray2Samples, 0x0001, 1); + RequireGrayTransparency(2, gray2Samples, 0x0005, 1); + RequireGrayTransparency(2, gray2Samples, 0x0100, 0); + RequireGrayTransparency(2, gray2Samples, 0x0101, 1); + RequireGrayTransparency(2, gray2Samples, 0xFF05, 1); + RequireGrayTransparency(2, gray2Samples, 0xFFFF, 3); + RequireGrayOpaque(2, gray2Samples); + + const std::uint8_t gray4Samples[] = {0, 3, 10, 15, 1}; + RequireGrayTransparency(4, gray4Samples, 0x0003, 3); + RequireGrayTransparency(4, gray4Samples, 0x0013, 3); + RequireGrayTransparency(4, gray4Samples, 0x0100, 0); + RequireGrayTransparency(4, gray4Samples, 0x0103, 3); + RequireGrayTransparency(4, gray4Samples, 0xFF13, 3); + RequireGrayTransparency(4, gray4Samples, 0xFFFF, 15); + RequireGrayOpaque(4, gray4Samples); + RequireTruncatedTransparency(); + const auto interlaced = Decoder::Png::ParseHeader(HeaderOnly(3, 4, 8, 6, 1)); Require(interlaced.has_value() && interlaced->interlaced && !interlaced->hasTransparency); Require(!Decoder::Png::ParseHeader(HeaderOnly(0, 4, 8, 6, 0)).has_value()); From 2f8177d7b4a983cacd072d22c1221db344a2fa6c Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 12:24:36 -0300 Subject: [PATCH 175/318] fix(libkernel): keep the range sceKernelReleaseFlexibleMemory releases mapped Measured on firmware 13.60: the released range stays mapped with its protection and contents, and a fixed no-overwrite map over it fails with ENOMEM; a null or unaligned address, an unaligned length or a range that wraps returns EINVAL, checked before a zero length, which returns 0, as does a range with nothing mapped. Co-Authored-By: Claude Opus 5.5 --- .../prx/libkernel/DirectMemory/Export.cpp | 6 +++++- core/libs/tests/GuestMemory.cpp | 21 ++++++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index bd93f135d1..1afee54e9f 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -320,7 +320,11 @@ int APS5_VABI sceKernelMmap(void* addr, size_t len, int prot, int flags, int fd, } int APS5_VABI sceKernelReleaseFlexibleMemory(void* addr, size_t len) { - return sceKernelMunmap(reinterpret_cast(addr), len); + const auto start = reinterpret_cast(addr); + if (start == 0 || (start & (PS5_PAGE_SIZE - 1)) != 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || len > std::numeric_limits::max() - start) + return SCE_KERNEL_ERROR_EINVAL; + if (len != 0) _releaseFlexible(start, len); + return 0; } int APS5_VABI sceKernelReleaseDirectMemory(int64_t start, size_t len) { diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index d03a35de70..72532181e1 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -113,11 +113,30 @@ static void CheckReleaseFlexibleMemory() { Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0 && mapped != nullptr); static_cast(mapped)[length - 1] = 1; Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); + auto* bytes = static_cast(mapped); + Require(sceKernelMprotect(mapped, 0x4000, 1) == 0); Require(sceKernelReleaseFlexibleMemory(mapped, length) == 0); Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapped, 0, &info, sizeof(info)) == 0 && info.protection == 1); + Require(sceKernelVirtualQuery(static_cast(mapped) + length - 1, 0, &info, sizeof(info)) == 0 && info.protection == 3); + Require(bytes[length - 1] == 1); + bytes[length - 2] = 2; + Require(sceKernelReleaseFlexibleMemory(mapped, length) == 0); + Require(sceKernelReleaseFlexibleMemory(mapped, 0) == 0); + Require(sceKernelReleaseFlexibleMemory(reinterpret_cast(std::uintptr_t{0x7ff000000000}), length) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + Require(sceKernelReleaseFlexibleMemory(nullptr, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseFlexibleMemory(static_cast(mapped) + 1, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseFlexibleMemory(mapped, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseFlexibleMemory(reinterpret_cast(~std::uintptr_t{0x3fff}), 0x8000) == SCE_KERNEL_ERROR_EINVAL); void* again = mapped; - Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x90) == 0 && again == mapped); + Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x90) == SCE_KERNEL_ERROR_ENOMEM); + Require(bytes[length - 1] == 1 && bytes[length - 2] == 2); + again = mapped; + Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x10) == 0 && again == mapped); Require(static_cast(again)[length - 1] == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); Require(sceKernelMunmap(again, length) == 0); } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 08c6612c5f..62f1f84860 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -50,7 +50,7 @@ - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Not verified on the console. PPSA34063 imports both and fails to load without them - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. -- [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified +- [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - how the console handles invalid arguments is not measured. A negative `search_start`, a `search_end` at or below `search_start` and an alignment that overflows when rounded up to 16 KiB return `EAGAIN` (`0x80020023`); bounds that are not multiples of 16 KiB are rounded inward, the alignment is raised to a multiple of 16 KiB and need not be a power of two, and a `search_end` past the pool is cut to it. fpPS4's HLE wrapper on its trunk branch refuses all of these with `EINVAL` ([ps4_map_mm.pas](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_map_mm.pas#L558-L570)) - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits From 0db5b78405844f06a9f8bcd12444afeaa9831fa0 Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 12:44:03 -0300 Subject: [PATCH 176/318] test(libkernel): cover 4 KiB offsets in sceKernelReleaseFlexibleMemory Measured on firmware 13.60: an address or length aligned to 4 KiB but not to 16 KiB returns EINVAL. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestMemory.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 72532181e1..70aede6349 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -129,6 +129,8 @@ static void CheckReleaseFlexibleMemory() { Require(sceKernelReleaseFlexibleMemory(nullptr, 0) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelReleaseFlexibleMemory(static_cast(mapped) + 1, 0) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelReleaseFlexibleMemory(mapped, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseFlexibleMemory(static_cast(mapped) + 0x1000, 0x4000) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseFlexibleMemory(mapped, 0x6000) == SCE_KERNEL_ERROR_EINVAL); Require(sceKernelReleaseFlexibleMemory(reinterpret_cast(~std::uintptr_t{0x3fff}), 0x8000) == SCE_KERNEL_ERROR_EINVAL); void* again = mapped; Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x90) == SCE_KERNEL_ERROR_ENOMEM); From c399333e7aab4af43bd7a991b567bc80fd158dca Mon Sep 17 00:00:00 2001 From: orezraey Date: Wed, 21 Oct 2026 13:03:46 -0300 Subject: [PATCH 177/318] docs(libkernel): record that released flexible pages keep their host memory Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 62f1f84860..48a25923c7 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -50,7 +50,7 @@ - [sceKernelGetModuleInfo](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it - [sceKernelGetModuleList2 and sceKernelGetModuleInfo2](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the title-module variants follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/kernel/process.cpp): `sceKernelGetModuleList2` lists what `sceKernelGetModuleList` lists without system libraries, and `sceKernelGetModuleInfo2` answers `EPERM` for a system library after the `EFAULT`, `EINVAL` and `ESRCH` checks of `sceKernelGetModuleInfo`. A system library here is a loaded `.prx` image that is not a relinked title module (`.guest.prx`); the main program and the title's own modules are listed. Not verified on the console. PPSA34063 imports both and fails to load without them - [sceKernelGetLibkernelTextLocation](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the `(void** address, std::size_t* size)` signature and the choice of the main program's first segment follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_libkernel.pas#L338-L348); whether the console reports that segment, the text of libkernel itself or the calling module's is unverified. The segment is the first one `sceKernelGetModuleInfo` reports for the main program, and 0 is returned. A null argument throws. -- [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. +- [sceKernelReleaseFlexibleMemory](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - on a PS5 (firmware 13.60) the released range stays mapped, with its protection and contents (64 MiB checked after 0.5 s), and a fixed no-overwrite map over it fails with `ENOMEM`, as here. What it does to the flexible budget was not measured, because a payload process has none (`sceKernelAvailableFlexibleMemorySize` did not change on map or release); here the released bytes return to the budget while the pages stay mapped, and a later map over them charges them again. The host pages behind a released range stay committed until `sceKernelMunmap`, as `madvise` keeps them for every advice; freeing them would lose the contents the console still returns. - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [sceKernelAvailableDirectMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - how the console handles invalid arguments is not measured. A negative `search_start`, a `search_end` at or below `search_start` and an alignment that overflows when rounded up to 16 KiB return `EAGAIN` (`0x80020023`); bounds that are not multiples of 16 KiB are rounded inward, the alignment is raised to a multiple of 16 KiB and need not be a power of two, and a `search_end` past the pool is cut to it. fpPS4's HLE wrapper on its trunk branch refuses all of these with `EINVAL` ([ps4_map_mm.pas](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/kernel/ps4_map_mm.pas#L558-L570)) - [Memory protection bits 0x100 and 0x200](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits From 5e03230c28a3f466f6007ea735884074cc30a794 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 10:15:25 -0400 Subject: [PATCH 178/318] fix(libc): compute atanf with FreeBSD msun's algorithm, as titles' libc.prx does atanf returned the host atan, so its last bit depended on the host: on Windows it differs from the libc.prx that titles ship on 6,481,820 of the 2^32 floats. That libc.prx is msun's s_atanf.c, which gives the same bits as PPSA07923's, PPSA05916's and PPSA13934's copies for every argument. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 45 ++++++++++++++++++++++++++++++++- core/libs/tests/GuestMath.cpp | 16 ++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 37616b7be3..15073bc0c0 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -539,7 +539,50 @@ void APS5_VABI sincos_nid_postfix(double x, double* sinp, double* cosp) { *cosp = std::cos(x); } -float APS5_VABI atanf_nid_postfix(float x) { return std::atan(x); } +float APS5_VABI atanf_nid_postfix(float x) { + static constexpr float atanhi[] = {static_cast(4.6364760399e-01), static_cast(7.8539812565e-01), static_cast(9.8279368877e-01), static_cast(1.5707962513e+00)}; + static constexpr float atanlo[] = {static_cast(5.0121582440e-09), static_cast(3.7748947079e-08), static_cast(3.4473217170e-08), static_cast(7.5497894159e-08)}; + static constexpr float aT[] = {static_cast(3.3333328366e-01), static_cast(-1.9999158382e-01), static_cast(1.4253635705e-01), static_cast(-1.0648017377e-01), static_cast(6.1687607318e-02)}; + const std::int32_t hx = std::bit_cast(x); + const std::int32_t ix = hx & 0x7fffffff; + if (ix >= 0x4c800000) { + if (ix > 0x7f800000) { + return x + x; + } + const volatile float lo = atanlo[3]; + return hx > 0 ? atanhi[3] + lo : -atanhi[3] - lo; + } + int id = -1; + if (ix < 0x3ee00000) { + if (ix < 0x39800000) { + return x; + } + } else { + x = std::fabs(x); + if (ix < 0x3f300000) { + id = 0; + x = (2.0f * x - 1.0f) / (2.0f + x); + } else if (ix < 0x3f980000) { + id = 1; + x = (x - 1.0f) / (x + 1.0f); + } else if (ix < 0x401c0000) { + id = 2; + x = (x - 1.5f) / (1.0f + 1.5f * x); + } else { + id = 3; + x = -1.0f / x; + } + } + const float z = x * x; + const float w = z * z; + const float s1 = z * (aT[0] + w * (aT[2] + w * aT[4])); + const float s2 = w * (aT[1] + w * aT[3]); + if (id < 0) { + return x - x * (s1 + s2); + } + const float result = atanhi[id] - ((x * (s1 + s2) - atanlo[id]) - x); + return hx < 0 ? -result : result; +} double APS5_VABI atan2_nid_postfix(double y, double x) { return std::atan2(y, x); } float APS5_VABI powf_nid_postfix(float base, float exp) { if (base == 1.0f) return 1.0f; diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 04541e7c79..be1b0c1df6 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -83,6 +83,7 @@ float APS5_VABI asinf_nid_postfix(float); float APS5_VABI acosf_nid_postfix(float); double APS5_VABI asin_nid_postfix(double); float APS5_VABI atan2f_nid_postfix(float, float); +float APS5_VABI atanf_nid_postfix(float); float APS5_VABI hypotf_nid_postfix(float, float); double APS5_VABI hypot_nid_postfix(double, double); double APS5_VABI fdim_nid_postfix(double, double); @@ -498,6 +499,20 @@ static void CheckFloatUnderflowErrno() { Require(strtod_nid_postfix("2.2250738585072014e-308", &end) == 0x1p-1022 && errno == 0); } +static void CheckAtanfConsoleBits() { + constexpr std::uint32_t cases[][2] = { + {0x3e80000bu, 0x3e7adbc5u}, {0x3e0007c9u, 0x3dfebd2au}, {0xbdccce4cu, 0xbdcc2090u}, + {0x3ef00334u, 0x3ee07046u}, {0x3f40077eu, 0x3f24c148u}, {0x3fa009a5u, 0x3f656b6au}, + {0x4080055cu, 0x3fa9b5a8u}, {0xbf12345eu, 0xbf04d718u}, {0x3f800000u, 0x3f490fdbu}, + {0x4c800000u, 0x3fc90fdbu}, {0xcc800000u, 0xbfc90fdbu}, {0x7f800000u, 0x3fc90fdbu}, + {0xff800000u, 0xbfc90fdbu}, {0x7fc00001u, 0x7fc00001u}, {0x80000000u, 0x80000000u}, + {0x00000001u, 0x00000001u}, {0x39000000u, 0x39000000u}, + }; + for (const auto& [input, expected] : cases) { + Require(std::bit_cast(atanf_nid_postfix(std::bit_cast(input))) == expected); + } +} + static void CheckHypotSpecialValue() { Require(std::bit_cast(hypot_nid_postfix(INFINITY, std::bit_cast(0x7ff0000000000001ull))) == 0x7ff0000000000000ull); Require(std::bit_cast(hypot_nid_postfix(std::bit_cast(0x7ff0000000000001ull), INFINITY)) == 0x7ff0000000000000ull); @@ -1590,6 +1605,7 @@ int main() { CheckSinhf(); CheckAtanhf(); CheckHypotSpecialValue(); + CheckAtanfConsoleBits(); CheckFloatUnderflowErrno(); CheckFmodErrno(); CheckDoubleClassification(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ce70a5a958..c05f88b683 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -79,6 +79,7 @@ - [strtod, strtof, strtold](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `strtod` and `wcstod` (and `atof`, which calls `strtod` here) read a `NAN(...)` sequence as FreeBSD 11's gdtoa [`hexnan`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/hexnan.c) does: hexadecimal digits with an optional `0x`, blanks starting a new 32-bit word, the first 16 digits kept and the payload cut to 52 bits, a zero payload made 1, any other character giving the default NaN but consuming up to the closing parenthesis, and an unterminated sequence keeping its payload while consuming only `nan`. The payload goes under the quiet bit with the sign, as in gdtoa's [`strtod`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/contrib/gdtoa/strtod.c#L316-L328). `strtof`, `wcstof`, `strtold` and `wcstold` return the positive default quiet NaN (`0x7fc00000`; exponent `0x7fff`, significand `0xc000000000000000`) whatever the sign and the sequence, and consume a sequence of letters, digits and underscores up to its closing parenthesis. The console's `libSceLibcInternal.sprx`, called from a payload process on a PS5 (firmware 12.70), gives these results for the 64 NaN inputs tried through `strtod` and `strtof`, 50 of them also through `strtold` and `wcstod`, and 18 through `wcstof` and `wcstold`, and, through `strtod`, for `nan(1 2)` and for `nan( ` followed by NUL and `)`; that a title's own `libc.prx` does the same is assumed. In `hexnan`, a blank after a payload digit, while a 32-bit word of the payload is still free, starts a blank skip that has no NUL check, so when only blanks follow up to the terminating NUL the console's `strtod` reads past it: `nan(1 ` followed by NUL and `)` consumes the `)` after the NUL, so the result depends on the bytes after the string, and here that case throws. A blank right after `nan(` is skipped with a NUL check, and the console stopped at the NUL there - [remquol](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) leaves `*quo` unchanged when the result is NaN; the `libc.prx` bundled with PPSA08427 writes a value that depends on earlier x87 state (`remquol(1, 0)` stored 0 on a first call and 4 on later ones) - [Long double math in MathExact.cpp](../../../core/libs/prx/libc/src/MathExact.cpp) (libc) was compared with the `libc.prx` bundled with PPSA08427 only for canonical 80-bit encodings; unnormals, pseudo-denormals and pseudo-infinities passed to arithmetic functions (not the classifiers) follow the host x87 instead of the console +- [atanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_atanf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_atanf.c), with `atanlo[3]` read at run time as msun's `volatile` does. Tiny arguments return `x` without raising `FE_INEXACT`. It gives the same bits as the `atanf` of the `libc.prx` bundled with PPSA07923, PPSA05916 and PPSA13934 for all 2^32 arguments, run on a Windows host, and none of those sets `errno`. Whether other titles' builds differ is unverified. - [rename, sceKernelRename](../../../core/libs/prx/libc/include/RenameDot.hpp) (libc, libkernel) fail a last path component of `.` or `..` with `EINVAL`, after FreeBSD 11.4's [`lookup`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_lookup.c), which refuses `.` and, through its `ISDOTDOT` and `ISLASTCN` flags, `..` as the last component for the `DELETE` and `RENAME` operations. `rmdir("dir/.")`, `unlink("dir/.")` and the same calls with `..` follow the same lookup rule in FreeBSD but are not changed here: the guest still resolves the dot component away and acts on `dir` or its parent. Both arguments are checked before any host access, so `rename` of a missing source onto `dir/.` answers `EINVAL`, where FreeBSD's [`kern_renameat`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/vfs_syscalls.c) looks the source up first and answers `ENOENT`. A backslash counts as a path separator here (the guest path resolver turns it into `/`); FreeBSD treats it as an ordinary name character. None of this was measured on a console. - [nan and nanf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return fixed quiet NaN values (`0x7fc00000` and `0x7ff8000000000000`), ignoring the tag argument as observed in the console's `libc.prx` (both exports load a global constant via `mov rax,[rip+..]; vmovsd xmm0,[rax]; ret`); VLC (PPSA85300) stops on nanf at start-up after strsep. - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified From a22e959cd8b1cec415812d625b17ffd983d57a68 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 11:46:40 -0400 Subject: [PATCH 179/318] fix(libc): compute logf with FreeBSD msun's algorithm, as titles' libc.prx does logf returned the host log, so its last bit and errno depended on the host: on Windows the bits differ from the libc.prx that titles ship on 13,225,175 of the 2^32 floats. That libc.prx is msun's e_logf.c, setting ERANGE for +-0 and EDOM for every argument with the sign bit set. The port gives the same bits and errno as PPSA07923's and PPSA05916's copies for every argument; the FMA-contracted copy of PPSA13934 differs by 1 ulp on 961,881. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Math.cpp | 57 ++++++++++++++++++++++++++++++++- core/libs/tests/GuestMath.cpp | 22 +++++++++++++ docs/dev/debt/libc.md | 1 + 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 37616b7be3..d14b4118d3 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -572,7 +572,62 @@ double APS5_VABI __powidf2_nid_postfix(double base, int exponent) { } float APS5_VABI expf_nid_postfix(float x) { return std::exp(x); } float APS5_VABI exp2f_nid_postfix(float x) { return std::exp2(x); } -float APS5_VABI logf_nid_postfix(float x) { return std::log(x); } +float APS5_VABI logf_nid_postfix(float x) { + const float ln2_hi = static_cast(6.9313812256e-01); + const float ln2_lo = static_cast(9.0580006145e-06); + const float two25 = static_cast(3.355443200e+07); + const float Lg1 = 0xaaaaaa.0p-24f; + const float Lg2 = 0xccce13.0p-25f; + const float Lg3 = 0x91e9ee.0p-25f; + const float Lg4 = 0xf89e26.0p-26f; + static volatile float vzero = 0.0f; + std::int32_t k = 0; + std::int32_t ix = std::bit_cast(x); + if (ix < 0x00800000) { + if ((ix & 0x7fffffff) == 0) { + errno = ERANGE; + return -two25 / vzero; + } + if (ix < 0) { + errno = EDOM; + return (x - x) / 0.0f; + } + k -= 25; + x *= two25; + ix = std::bit_cast(x); + } + if (ix >= 0x7f800000) { + return x + x; + } + k += (ix >> 23) - 127; + ix &= 0x007fffff; + std::int32_t i = (ix + (0x95f64 << 3)) & 0x800000; + x = std::bit_cast(ix | (i ^ 0x3f800000)); + k += i >> 23; + const float f = x - 1.0f; + const float dk = static_cast(k); + if ((0x007fffff & (0x8000 + ix)) < 0xc000) { + if (f == 0.0f) { + return k == 0 ? 0.0f : dk * ln2_hi + dk * ln2_lo; + } + const float R = f * f * (0.5f - static_cast(0.33333333333333333) * f); + return k == 0 ? f - R : dk * ln2_hi - ((R - dk * ln2_lo) - f); + } + const float s = f / (2.0f + f); + const float z = s * s; + i = ix - (0x6147a << 3); + const float w = z * z; + const std::int32_t j = (0x6b851 << 3) - ix; + const float t1 = w * (Lg2 + w * Lg4); + const float t2 = z * (Lg1 + w * Lg3); + i |= j; + const float R = t2 + t1; + if (i > 0) { + const float hfsq = 0.5f * f * f; + return k == 0 ? f - (hfsq - s * (hfsq + R)) : dk * ln2_hi - ((hfsq - (s * (hfsq + R) + dk * ln2_lo)) - f); + } + return k == 0 ? f - s * (f - R) : dk * ln2_hi - ((s * (f - R) - dk * ln2_lo) - f); +} float APS5_VABI log2f_nid_postfix(float x) { return std::log2(x); } double APS5_VABI log10_nid_postfix(double x) { return std::log10(x); } float APS5_VABI ldexpf_nid_postfix(float x, int exp) { return std::ldexp(x, exp); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 04541e7c79..cf6dacc48f 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -125,6 +125,7 @@ float APS5_VABI nextafterf_nid_postfix(float, float); double APS5_VABI nextafter_nid_postfix(double, double); double APS5_VABI frexp_nid_postfix(double, int*); float APS5_VABI frexpf_nid_postfix(float, int*); +float APS5_VABI logf_nid_postfix(float); std::int64_t APS5_VABI lround_nid_postfix(double); std::div_t APS5_VABI div_nid_postfix(int, int); double APS5_VABI sinh_nid_postfix(double); @@ -1058,6 +1059,26 @@ static void RequireBytes(const void* constant, std::initializer_list(logf_nid_postfix(std::bit_cast(item.input))) == item.expected); + Require(errno == (item.error != 0 ? item.error : 1234)); + } +} + static void CheckDinkumwareConstants() { RequireBytes(&_Inf_nid_postfix, {0, 0, 0, 0, 0, 0, 0xf0, 0x7f}); RequireBytes(&_Denorm_nid_postfix, {1}); @@ -1589,6 +1610,7 @@ int main() { CheckScaledHyperbolic(); CheckSinhf(); CheckAtanhf(); + CheckLogfConsoleBits(); CheckHypotSpecialValue(); CheckFloatUnderflowErrno(); CheckFmodErrno(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ce70a5a958..bf4c6ce2ba 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -64,6 +64,7 @@ - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. +- [logf](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_logf.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_logf.c), with `vzero` read at run time as msun's `volatile` read does. As in the `libc.prx` that titles ship, `errno` is set to `ERANGE` for ±0 and to `EDOM` for every argument with the sign bit set, negative NaNs and -inf included; positive NaNs and +inf leave it unchanged. Floating-point exceptions are not compared. It gives the same bits and `errno` as the `logf` of the `libc.prx` bundled with PPSA07923 and PPSA05916 for all 2^32 arguments, run on a Windows host. The FMA-contracted build bundled with PPSA13934 differs from it by 1 ulp on 961,881 arguments, so no single port matches every title's last bit. - Windows direct-memory backing reserves a paging-file section. A [direct memory page](../../../core/libs/prx/libc/include/WindowsMappings.hpp) mapped at one guest address is private write-watched memory at that address and is not committed in the section; a page mapped at a second address moves its whole resident run into the section (committed there) and every address becomes a view of it, tracked by write protection. An unmapped resident page that is not all zeros is kept in a private reservation until it is mapped again. Committed section contents remain across unmap/remap and are released when the backing and its remaining views are destroyed. A host import alias over part of a resident run is refused. Linux retains its existing backing. Peak gameplay memory and Windows versions other than the tested host remain unverified. - Stable Windows resident runs of at least 256 KiB use temporary read-only protection for clean write-watch proofs. The first CPU write restores the logical protection of the whole run; that run remains on native write watching until it is remapped, so repeatedly rewritten runs stay writable. Native kernel writes into guest buffers must hold [GuestArena::HostWrite](../../../core/libs/prx/libc/include/GuestArena.hpp), as shared views already require: an unbracketed WinAPI ReadFile can fail with ERROR_NOACCESS while the resident proof is armed. Guest read, pread and socket receive paths already bracket their destinations. GPU host-import access suppresses resident rearming without inventing shared-page stores. This is a host integration requirement, not a change to guest logical protection. From 7140ec1591c1d8569448abd85d70e7d97231b1a0 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Thu, 22 Oct 2026 14:56:53 +0300 Subject: [PATCH 180/318] feat(np): answer and declare the PS4 NP imports of CUSA03312 NpMatching2 keeps its initialized state. sceNpSignalingDeactivateConnection and sceInvitationDialogClose, which these titles also import, are declared and throw. Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 10 +++ .../prx/libSceInvitationDialog/Export.cpp | 5 ++ core/libs/prx/libSceNpMatching2/Export.cpp | 39 +++++++++-- core/libs/prx/libSceNpSignaling/Export.cpp | 5 ++ core/libs/tests/GuestNpPs4Imports.cpp | 66 +++++++++++++++++++ docs/dev/debt/libSceInvitationDialog.md | 5 ++ docs/dev/debt/libSceNpMatching2.md | 5 ++ docs/dev/debt/libSceNpSignaling.md | 5 ++ 8 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 core/libs/tests/GuestNpPs4Imports.cpp create mode 100644 docs/dev/debt/libSceInvitationDialog.md create mode 100644 docs/dev/debt/libSceNpMatching2.md create mode 100644 docs/dev/debt/libSceNpSignaling.md diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 0472176eb2..586f216d20 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -408,6 +408,16 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() + add_test_executable(guest_np_ps4_imports_tests tests/GuestNpPs4Imports.cpp) + target_include_directories(guest_np_ps4_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(guest_np_ps4_imports_tests PRIVATE libSceNpMatching2 libSceNpSignaling libc) + configure_windows_unwind(guest_np_ps4_imports_tests) + add_test(NAME guest_np_ps4_imports COMMAND guest_np_ps4_imports_tests) + if(WIN32) + set_tests_properties(guest_np_ps4_imports PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp) target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libSceVideoRecordingP libc) diff --git a/core/libs/prx/libSceInvitationDialog/Export.cpp b/core/libs/prx/libSceInvitationDialog/Export.cpp index 8ae801bc26..6292323d0c 100644 --- a/core/libs/prx/libSceInvitationDialog/Export.cpp +++ b/core/libs/prx/libSceInvitationDialog/Export.cpp @@ -28,4 +28,9 @@ int APS5_VABI sceInvitationDialogUpdateStatus() { return 0; } +int APS5_VABI sceInvitationDialogClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpMatching2/Export.cpp b/core/libs/prx/libSceNpMatching2/Export.cpp index 2746971c77..d7fdbb3817 100644 --- a/core/libs/prx/libSceNpMatching2/Export.cpp +++ b/core/libs/prx/libSceNpMatching2/Export.cpp @@ -1,6 +1,32 @@ +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static constexpr int SCE_NP_MATCHING2_ERROR_ALREADY_INITIALIZED = static_cast(0x80550C02); +static constexpr int SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED = static_cast(0x80550C03); +static constexpr int SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT = static_cast(0x80550C0A); + +namespace { + +struct NpMatching2InitializeParameter { + std::uint64_t poolSize; + std::uint64_t cpuAffinityMask; + std::int32_t threadPriority; + std::uint8_t padding[4]; + std::uint64_t threadStackSize; + std::uint64_t size; + std::uint64_t sslPoolSize; +}; +static_assert(offsetof(NpMatching2InitializeParameter, size) == 0x20); +static_assert(sizeof(NpMatching2InitializeParameter) == 0x30); + +std::mutex matching2Mutex; +bool matching2Initialized = false; + +} + extern "C" { int APS5_VABI sceNpMatching2ContextStart() { @@ -58,8 +84,11 @@ int APS5_VABI sceNpMatching2GetWorldInfoList() { return 0; } -int APS5_VABI sceNpMatching2Initialize() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpMatching2Initialize(const NpMatching2InitializeParameter* param) { + std::lock_guard lock(matching2Mutex); + if (matching2Initialized) return SCE_NP_MATCHING2_ERROR_ALREADY_INITIALIZED; + if (!param || (param->size != offsetof(NpMatching2InitializeParameter, sslPoolSize) && param->size != sizeof(NpMatching2InitializeParameter))) return SCE_NP_MATCHING2_ERROR_INVALID_ARGUMENT; + matching2Initialized = true; return 0; } @@ -143,8 +172,10 @@ int APS5_VABI sceNpMatching2SignalingGetConnectionInfo() { return 0; } -int APS5_VABI sceNpMatching2Terminate() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpMatching2Terminate(void) { + std::lock_guard lock(matching2Mutex); + if (!matching2Initialized) return SCE_NP_MATCHING2_ERROR_NOT_INITIALIZED; + matching2Initialized = false; return 0; } diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp index 8ab2a25170..fb5963de6e 100644 --- a/core/libs/prx/libSceNpSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -43,4 +43,9 @@ int APS5_VABI sceNpSignalingTerminateConnection() { return 0; } +int APS5_VABI sceNpSignalingDeactivateConnection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/tests/GuestNpPs4Imports.cpp b/core/libs/tests/GuestNpPs4Imports.cpp new file mode 100644 index 0000000000..9f2497a2f1 --- /dev/null +++ b/core/libs/tests/GuestNpPs4Imports.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpMatching2Initialize(const void* param); +int APS5_VABI sceNpMatching2Terminate(void); +int APS5_VABI sceNpSignalingDeactivateConnection(void); +} + +namespace { + +constexpr int Matching2AlreadyInitialized = static_cast(0x80550C02u); +constexpr int Matching2NotInitialized = static_cast(0x80550C03u); +constexpr int Matching2InvalidArgument = static_cast(0x80550C0Au); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "NpPs4Imports: %s\n", message); + std::abort(); + } +} + +template +void RequireNotImplemented(TCall call, const std::string& name) { + try { + call(); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()) == name + " not implemented", "a declared function threw another error"); + return; + } + Require(false, "a declared function returned"); +} + +void CheckMatching2() { + unsigned char param[0x30]{}; + const auto setSize = [&](std::uint64_t size) { std::memcpy(param + 0x20, &size, sizeof(size)); }; + Require(sceNpMatching2Terminate() == Matching2NotInitialized, "NpMatching2 terminated before initialization"); + Require(sceNpMatching2Initialize(nullptr) == Matching2InvalidArgument, "a null NpMatching2 parameter was accepted"); + setSize(0x20u); + Require(sceNpMatching2Initialize(param) == Matching2InvalidArgument, "an NpMatching2 parameter of size 0x20 was accepted"); + setSize(0x28u); + Require(sceNpMatching2Initialize(param) == 0, "an NpMatching2 parameter of size 0x28 was refused"); + Require(sceNpMatching2Initialize(param) == Matching2AlreadyInitialized, "NpMatching2 initialized twice"); + Require(sceNpMatching2Terminate() == 0, "NpMatching2 did not terminate"); + setSize(0x30u); + Require(sceNpMatching2Initialize(param) == 0, "an NpMatching2 parameter with an SSL pool was refused"); + Require(sceNpMatching2Terminate() == 0, "NpMatching2 did not terminate after the SSL form"); +} + +void CheckSignaling() { + RequireNotImplemented([] { sceNpSignalingDeactivateConnection(); }, "sceNpSignalingDeactivateConnection"); +} + +} + +int main() { + CheckMatching2(); + CheckSignaling(); + return 0; +} diff --git a/docs/dev/debt/libSceInvitationDialog.md b/docs/dev/debt/libSceInvitationDialog.md new file mode 100644 index 0000000000..e7687fa477 --- /dev/null +++ b/docs/dev/debt/libSceInvitationDialog.md @@ -0,0 +1,5 @@ +# libSceInvitationDialog technical debt + +### Unknown function info + +- [sceInvitationDialogClose](../../../core/libs/prx/libSceInvitationDialog/Export.cpp) (libSceInvitationDialog) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws diff --git a/docs/dev/debt/libSceNpMatching2.md b/docs/dev/debt/libSceNpMatching2.md new file mode 100644 index 0000000000..15a5abb1e0 --- /dev/null +++ b/docs/dev/debt/libSceNpMatching2.md @@ -0,0 +1,5 @@ +# libSceNpMatching2 technical debt + +### Functional + +- [sceNpMatching2Initialize, sceNpMatching2Terminate](../../../core/libs/prx/libSceNpMatching2/Export.cpp) (libSceNpMatching2) - only the library state is kept: initialization checks the parameter's `size` field (0x28, or 0x30 with an SSL pool) as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_matching2/np_matching2.cpp) does and starts no thread; a second initialization returns `ALREADY_INITIALIZED` and a termination without one `NOT_INITIALIZED`. The pool, thread and SSL settings are not used: no matching server is reached. Not measured on a console diff --git a/docs/dev/debt/libSceNpSignaling.md b/docs/dev/debt/libSceNpSignaling.md new file mode 100644 index 0000000000..132922c319 --- /dev/null +++ b/docs/dev/debt/libSceNpSignaling.md @@ -0,0 +1,5 @@ +# libSceNpSignaling technical debt + +### Unknown function info + +- [sceNpSignalingDeactivateConnection](../../../core/libs/prx/libSceNpSignaling/Export.cpp) (libSceNpSignaling) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws From 26469c711a98a4da01b32b92a39150c1dd8c85c3 Mon Sep 17 00:00:00 2001 From: Savvanis Spyros Date: Tue, 20 Oct 2026 05:43:49 +0300 Subject: [PATCH 181/318] feat(libSceNpTrophy): answer game, base group and trophy info from the trophy package Co-Authored-By: Claude Opus 5.5 --- core/libs/CMakeLists.txt | 2 +- core/libs/prx/libSceNpTrophy/Export.cpp | 153 +++++++++++++++++++++++- core/libs/tests/GuestNpPs4Imports.cpp | 131 ++++++++++++++++++++ docs/dev/debt/libSceNpTrophy.md | 8 +- 4 files changed, 286 insertions(+), 8 deletions(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 586f216d20..51ea18a582 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -410,7 +410,7 @@ if(BUILD_TESTING) add_test_executable(guest_np_ps4_imports_tests tests/GuestNpPs4Imports.cpp) target_include_directories(guest_np_ps4_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(guest_np_ps4_imports_tests PRIVATE libSceNpMatching2 libSceNpSignaling libc) + target_link_libraries(guest_np_ps4_imports_tests PRIVATE libSceNpMatching2 libSceNpSignaling libSceNpTrophy libc) configure_windows_unwind(guest_np_ps4_imports_tests) add_test(NAME guest_np_ps4_imports COMMAND guest_np_ps4_imports_tests) if(WIN32) diff --git a/core/libs/prx/libSceNpTrophy/Export.cpp b/core/libs/prx/libSceNpTrophy/Export.cpp index aad30b5af0..c7823fd4c5 100644 --- a/core/libs/prx/libSceNpTrophy/Export.cpp +++ b/core/libs/prx/libSceNpTrophy/Export.cpp @@ -31,10 +31,83 @@ constexpr int NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED = static_cast(0x8055161au constexpr int NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED = static_cast(0x80551626u); constexpr int NP_TROPHY_INVALID_ID = -1; constexpr int NP_TROPHY_NUM_MAX = 128; +constexpr int NP_TROPHY_GRADE_UNKNOWN = 0; +constexpr int NP_TROPHY_BASE_GAME_GROUP_ID = -1; constexpr std::uint32_t TrpMagic = 0xdca24d00u; constexpr std::size_t TrpHeaderBytes = 0x60; constexpr std::size_t TrpEntryBytes = 0x40; +struct GameDetails { + std::uint64_t size; + std::uint32_t numGroups; + std::uint32_t numTrophies; + std::uint32_t numPlatinum; + std::uint32_t numGold; + std::uint32_t numSilver; + std::uint32_t numBronze; + char title[128]; + char description[1024]; +}; +static_assert(sizeof(GameDetails) == 0x4a0); + +struct GameData { + std::uint64_t size; + std::uint32_t unlockedTrophies; + std::uint32_t unlockedPlatinum; + std::uint32_t unlockedGold; + std::uint32_t unlockedSilver; + std::uint32_t unlockedBronze; + std::uint32_t progressPercentage; +}; +static_assert(sizeof(GameData) == 0x20); + +struct GroupDetails { + std::uint64_t size; + std::int32_t groupId; + std::uint32_t numTrophies; + std::uint32_t numPlatinum; + std::uint32_t numGold; + std::uint32_t numSilver; + std::uint32_t numBronze; + char title[128]; + char description[1024]; +}; +static_assert(sizeof(GroupDetails) == 0x4a0); + +struct GroupData { + std::uint64_t size; + std::int32_t groupId; + std::uint32_t unlockedTrophies; + std::uint32_t unlockedPlatinum; + std::uint32_t unlockedGold; + std::uint32_t unlockedSilver; + std::uint32_t unlockedBronze; + std::uint32_t progressPercentage; + std::uint8_t reserved[4]; +}; +static_assert(sizeof(GroupData) == 0x28); + +struct TrophyDetails { + std::uint64_t size; + std::int32_t trophyId; + std::int32_t trophyGrade; + std::int32_t groupId; + bool hidden; + std::uint8_t reserved[3]; + char name[128]; + char description[1024]; +}; +static_assert(sizeof(TrophyDetails) == 0x498); + +struct TrophyData { + std::uint64_t size; + std::int32_t trophyId; + bool unlocked; + std::uint8_t reserved[3]; + std::uint64_t timestamp; +}; +static_assert(sizeof(TrophyData) == 0x18); + struct TrophyFlagArray { std::uint32_t bits[NP_TROPHY_NUM_MAX / 32]; }; @@ -114,6 +187,21 @@ int Lookup(int contextId, int handle, Context*& context) { return 0; } +int Groups(const Context& context) { + int groups = 0; + for (int id = 0; id < NP_TROPHY_NUM_MAX; ++id) { + char icon[16]; + std::snprintf(icon, sizeof(icon), "GR%03d.PNG", id); + if (!context.entries.contains(icon)) break; + groups = id + 1; + } + return groups; +} + +std::uint32_t Progress(const Context& context) { + return context.trophies == 0 ? 0u : static_cast(context.unlocked.size() * 100 / static_cast(context.trophies)); +} + int CopyIcon(const Context& context, const std::string& name, void* buffer, std::uint64_t* size) { const auto entry = context.entries.find(name); if (entry == context.entries.end()) return NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND; @@ -250,18 +338,71 @@ int APS5_VABI sceNpTrophyGetTrophyIcon(int contextId, int handle, int trophyId, return CopyIcon(*context, icon, buffer, size); } -int APS5_VABI sceNpTrophyGetGameInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpTrophyGetGameInfo(int contextId, int handle, GameDetails* details, GameData* data) { + if (!details && !data) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + if ((details && details->size != sizeof(GameDetails)) || (data && data->size != sizeof(GameData))) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (details) { + std::memset(reinterpret_cast(details) + sizeof(details->size), 0, sizeof(GameDetails) - sizeof(details->size)); + details->numGroups = static_cast(Groups(*context)); + details->numTrophies = static_cast(context->trophies); + } + if (data) { + std::memset(reinterpret_cast(data) + sizeof(data->size), 0, sizeof(GameData) - sizeof(data->size)); + data->unlockedTrophies = static_cast(context->unlocked.size()); + data->progressPercentage = Progress(*context); + } return 0; } -int APS5_VABI sceNpTrophyGetGroupInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpTrophyGetGroupInfo(int contextId, int handle, int groupId, GroupDetails* details, GroupData* data) { + if (!details && !data) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + if ((details && details->size != sizeof(GroupDetails)) || (data && data->size != sizeof(GroupData))) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (groupId != NP_TROPHY_BASE_GAME_GROUP_ID) { + if (groupId < 0 || groupId >= Groups(*context)) return NP_TROPHY_ERROR_INVALID_GROUP_ID; + NotImplemented_nid_no_patch(__func__); + } + if (details) { + std::memset(reinterpret_cast(details) + sizeof(details->size), 0, sizeof(GroupDetails) - sizeof(details->size)); + details->groupId = groupId; + details->numTrophies = static_cast(context->trophies); + } + if (data) { + std::memset(reinterpret_cast(data) + sizeof(data->size), 0, sizeof(GroupData) - sizeof(data->size)); + data->groupId = groupId; + data->unlockedTrophies = static_cast(context->unlocked.size()); + data->progressPercentage = Progress(*context); + } return 0; } -int APS5_VABI sceNpTrophyGetTrophyInfo() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpTrophyGetTrophyInfo(int contextId, int handle, int trophyId, TrophyDetails* details, TrophyData* data) { + if (!details && !data) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + if ((details && details->size != sizeof(TrophyDetails)) || (data && data->size != sizeof(TrophyData))) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; + if (Groups(*context) != 0) NotImplemented_nid_no_patch(__func__); + if (details) { + std::memset(reinterpret_cast(details) + sizeof(details->size), 0, sizeof(TrophyDetails) - sizeof(details->size)); + details->trophyId = trophyId; + details->trophyGrade = NP_TROPHY_GRADE_UNKNOWN; + details->groupId = NP_TROPHY_BASE_GAME_GROUP_ID; + } + if (data) { + std::memset(reinterpret_cast(data) + sizeof(data->size), 0, sizeof(TrophyData) - sizeof(data->size)); + data->trophyId = trophyId; + data->unlocked = context->unlocked.contains(trophyId); + } return 0; } diff --git a/core/libs/tests/GuestNpPs4Imports.cpp b/core/libs/tests/GuestNpPs4Imports.cpp index 9f2497a2f1..dd30ac0629 100644 --- a/core/libs/tests/GuestNpPs4Imports.cpp +++ b/core/libs/tests/GuestNpPs4Imports.cpp @@ -4,10 +4,21 @@ #include #include #include +#include +#include #include #include +#include +#include extern "C" { +int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options); +int APS5_VABI sceNpTrophyCreateHandle(int* handle); +int APS5_VABI sceNpTrophyRegisterContext(int context, int handle, std::uint64_t options); +int APS5_VABI sceNpTrophyUnlockTrophy(int context, int handle, int trophyId, int* platinumId); +int APS5_VABI sceNpTrophyGetGameInfo(int context, int handle, void* details, void* data); +int APS5_VABI sceNpTrophyGetGroupInfo(int context, int handle, int groupId, void* details, void* data); +int APS5_VABI sceNpTrophyGetTrophyInfo(int context, int handle, int trophyId, void* details, void* data); int APS5_VABI sceNpMatching2Initialize(const void* param); int APS5_VABI sceNpMatching2Terminate(void); int APS5_VABI sceNpSignalingDeactivateConnection(void); @@ -18,6 +29,54 @@ namespace { constexpr int Matching2AlreadyInitialized = static_cast(0x80550C02u); constexpr int Matching2NotInitialized = static_cast(0x80550C03u); constexpr int Matching2InvalidArgument = static_cast(0x80550C0Au); +constexpr int TrophyInvalidArgument = static_cast(0x80551604u); +constexpr int TrophyInvalidTrophyId = static_cast(0x8055160au); +constexpr int TrophyInvalidGroupId = static_cast(0x8055160bu); +constexpr int TrophyNotRegistered = static_cast(0x8055160fu); + +struct TrophyGameDetails { + std::uint64_t size; + std::uint32_t numGroups, numTrophies, numPlatinum, numGold, numSilver, numBronze; + char title[128]; + char description[1024]; +}; + +struct TrophyGameData { + std::uint64_t size; + std::uint32_t unlockedTrophies, unlockedPlatinum, unlockedGold, unlockedSilver, unlockedBronze, progressPercentage; +}; + +struct TrophyGroupDetails { + std::uint64_t size; + std::int32_t groupId; + std::uint32_t numTrophies, numPlatinum, numGold, numSilver, numBronze; + char title[128]; + char description[1024]; +}; + +struct TrophyGroupData { + std::uint64_t size; + std::int32_t groupId; + std::uint32_t unlockedTrophies, unlockedPlatinum, unlockedGold, unlockedSilver, unlockedBronze, progressPercentage; + std::uint8_t reserved[4]; +}; + +struct TrophyDetails { + std::uint64_t size; + std::int32_t trophyId, trophyGrade, groupId; + bool hidden; + std::uint8_t reserved[3]; + char name[128]; + char description[1024]; +}; + +struct TrophyData { + std::uint64_t size; + std::int32_t trophyId; + bool unlocked; + std::uint8_t reserved[3]; + std::uint64_t timestamp; +}; void Require(bool condition, const char* message) { if (!condition) { @@ -37,6 +96,77 @@ void RequireNotImplemented(TCall call, const std::string& name) { Require(false, "a declared function returned"); } +void WriteBigEndian(std::vector& bytes, std::size_t at, std::uint64_t value, std::size_t width) { + for (std::size_t index = 0; index < width; ++index) bytes[at + index] = static_cast(value >> (8u * (width - 1u - index))); +} + +void WriteTrophyPackage(const std::filesystem::path& path) { + const char* const names[] = {"TROPCONF.ESFM", "TROP000.PNG", "TROP001.PNG", "TROP002.PNG", "ICON0.PNG"}; + constexpr std::size_t count = sizeof(names) / sizeof(names[0]); + std::vector bytes(0x60 + count * 0x40 + count * 16, 0u); + WriteBigEndian(bytes, 0x00, 0xdca24d00u, 4); + WriteBigEndian(bytes, 0x08, bytes.size(), 8); + WriteBigEndian(bytes, 0x10, count, 4); + WriteBigEndian(bytes, 0x14, 0x40u, 4); + for (std::size_t index = 0; index < count; ++index) { + const auto entry = 0x60 + index * 0x40; + std::memcpy(bytes.data() + entry, names[index], std::strlen(names[index])); + WriteBigEndian(bytes, entry + 32, 0x60 + count * 0x40 + index * 16, 8); + WriteBigEndian(bytes, entry + 40, 16u, 8); + } + std::filesystem::create_directories(path.parent_path()); + std::ofstream(path, std::ios::binary).write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); +} + +void CheckTrophyInfo() { + constexpr int user = 0x10000000; + constexpr std::uint32_t label = 7u; + const std::filesystem::path package = "app0/sce_sys/trophy/trophy07.trp"; + const std::filesystem::path state = "_trophy/trophy07.268435456"; + std::filesystem::remove(state); + WriteTrophyPackage(package); + int context = -1; + int handle = -1; + Require(sceNpTrophyCreateContext(&context, user, label, 0u) == 0 && sceNpTrophyCreateHandle(&handle) == 0, "trophy context or handle not created"); + TrophyGameDetails game{}; + TrophyGameData gameData{}; + game.size = sizeof(game); + gameData.size = sizeof(gameData); + Require(sceNpTrophyGetGameInfo(context, handle, &game, &gameData) == TrophyNotRegistered, "game info answered before registration"); + Require(sceNpTrophyRegisterContext(context, handle, 0u) == 0, "trophy context not registered"); + Require(sceNpTrophyGetGameInfo(context, handle, nullptr, nullptr) == TrophyInvalidArgument, "game info without outputs accepted"); + game.size = sizeof(game) - 8u; + Require(sceNpTrophyGetGameInfo(context, handle, &game, nullptr) == TrophyInvalidArgument, "game details of a wrong size accepted"); + game.size = sizeof(game); + std::memset(game.title, 'x', sizeof(game.title)); + Require(sceNpTrophyGetGameInfo(context, handle, &game, &gameData) == 0, "game info refused"); + Require(game.numTrophies == 3u && game.numGroups == 0u && game.numPlatinum + game.numGold + game.numSilver + game.numBronze == 0u && game.title[0] == '\0', "game details are not the package's three trophies with unknown grades"); + Require(gameData.unlockedTrophies == 0u && gameData.progressPercentage == 0u, "game data reports progress before an unlock"); + int platinum = 0; + Require(sceNpTrophyUnlockTrophy(context, handle, 1, &platinum) == 0, "trophy 1 not unlocked"); + Require(sceNpTrophyGetGameInfo(context, handle, nullptr, &gameData) == 0 && gameData.unlockedTrophies == 1u && gameData.progressPercentage == 33u, "game data does not count the unlock"); + TrophyDetails trophy{}; + TrophyData trophyData{}; + trophy.size = sizeof(trophy); + trophyData.size = sizeof(trophyData); + Require(sceNpTrophyGetTrophyInfo(context, handle, 1, &trophy, &trophyData) == 0, "trophy info refused"); + Require(trophy.trophyId == 1 && trophy.trophyGrade == 0 && trophy.groupId == -1 && !trophy.hidden && trophy.name[0] == '\0' && trophyData.trophyId == 1 && trophyData.unlocked, "trophy 1 info is wrong"); + Require(sceNpTrophyGetTrophyInfo(context, handle, 0, nullptr, &trophyData) == 0 && trophyData.trophyId == 0 && !trophyData.unlocked, "trophy 0 reported unlocked"); + Require(sceNpTrophyGetTrophyInfo(context, handle, 3, &trophy, &trophyData) == TrophyInvalidTrophyId, "a trophy past the package accepted"); + TrophyGroupDetails group{}; + TrophyGroupData groupData{}; + group.size = sizeof(group); + groupData.size = sizeof(groupData); + Require(sceNpTrophyGetGroupInfo(context, handle, -1, &group, &groupData) == 0, "base game group refused"); + Require(group.groupId == -1 && group.numTrophies == 3u && groupData.groupId == -1 && groupData.unlockedTrophies == 1u && groupData.progressPercentage == 33u, "base game group info is wrong"); + Require(sceNpTrophyGetGroupInfo(context, handle, 0, &group, &groupData) == TrophyInvalidGroupId, "a group the package does not have accepted"); + std::filesystem::remove(state); + std::filesystem::remove(package); + std::error_code ignored; + for (auto directory = package.parent_path(); !directory.empty(); directory = directory.parent_path()) std::filesystem::remove(directory, ignored); + std::filesystem::remove(state.parent_path(), ignored); +} + void CheckMatching2() { unsigned char param[0x30]{}; const auto setSize = [&](std::uint64_t size) { std::memcpy(param + 0x20, &size, sizeof(size)); }; @@ -60,6 +190,7 @@ void CheckSignaling() { } int main() { + CheckTrophyInfo(); CheckMatching2(); CheckSignaling(); return 0; diff --git a/docs/dev/debt/libSceNpTrophy.md b/docs/dev/debt/libSceNpTrophy.md index c445f4e8d8..82ca475ea5 100644 --- a/docs/dev/debt/libSceNpTrophy.md +++ b/docs/dev/debt/libSceNpTrophy.md @@ -2,4 +2,10 @@ ### Unknown function info -- [sceNpTrophyRegisterContext, sceNpTrophyGetTrophyUnlockState, sceNpTrophyUnlockTrophy](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - the trophy definitions in `TROPCONF.ESFM` are encrypted, so the trophy count is the number of consecutive `TROPnnn.PNG` icons in the title's `/app0/sce_sys/trophy/trophyNN.trp`, and the calls that return names and details (`sceNpTrophyGetGameInfo`, `sceNpTrophyGetGroupInfo`, `sceNpTrophyGetTrophyInfo`) throw. Unlocks are kept per user and service label in `_trophy/trophyNN.` under the working directory, no platinum trophy is reported (`platinumId` is -1), and a locked trophy's icon fails with `TROPHY_NOT_UNLOCKED`. None of it is checked on a console. +- [sceNpTrophyRegisterContext, sceNpTrophyGetTrophyUnlockState, sceNpTrophyUnlockTrophy](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - the trophy definitions in `TROPCONF.ESFM` are encrypted, so the trophy count is the number of consecutive `TROPnnn.PNG` icons in the title's `/app0/sce_sys/trophy/trophyNN.trp`. Unlocks are kept per user and service label in `_trophy/trophyNN.` under the working directory, no platinum trophy is reported (`platinumId` is -1), and a locked trophy's icon fails with `TROPHY_NOT_UNLOCKED`. None of it is checked on a console. + +### Functional + +- [sceNpTrophyGetGameInfo](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - the trophy configuration `TROPCONF.ESFM` is encrypted and not read, so the game details report the trophy count taken from the `TROPnnn.PNG` icons and the group count taken from the `GRnnn.PNG` icons of the trophy package; the per-grade counts are 0, the title and description are empty, and the progress percentage is unlocked trophies over all trophies instead of the console's grade-weighted points +- [sceNpTrophyGetGroupInfo](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - only the base game group (id -1) is answered, with the same counts as the game; an id past the `GRnnn.PNG` icons returns `INVALID_GROUP_ID`, and an existing group throws because the trophies of a group are listed only in the encrypted configuration +- [sceNpTrophyGetTrophyInfo](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy) - the grade is reported as unknown, the trophy is never hidden, the name and description are empty and the unlock timestamp is 0; a package with groups throws, since a trophy's group is listed only in the encrypted configuration From 6710ba6702604ebd8f191ebdc76625e5ccae4e42 Mon Sep 17 00:00:00 2001 From: Federico Marras Date: Thu, 22 Oct 2026 22:49:04 +0200 Subject: [PATCH 182/318] feat(libSceNpScoreRanking): model the score requests offline The 15 exports threw. They now keep title contexts and requests, run shadPS4's argument checks in its order, and answer the record, ranking and board calls with SCE_NP_ERROR_SIGNED_OUT, which also finishes the request, as shadPS4 does for a user with no connection. --- .../prx/libSceNpScoreRanking/CMakeLists.txt | 16 ++ core/libs/prx/libSceNpScoreRanking/Export.cpp | 247 ++++++++++++++---- core/libs/tests/GuestNpScoreOffline.cpp | 223 ++++++++++++++++ docs/dev/debt/libSceNpScoreRanking.md | 5 + 4 files changed, 444 insertions(+), 47 deletions(-) create mode 100644 core/libs/prx/libSceNpScoreRanking/CMakeLists.txt create mode 100644 core/libs/tests/GuestNpScoreOffline.cpp create mode 100644 docs/dev/debt/libSceNpScoreRanking.md diff --git a/core/libs/prx/libSceNpScoreRanking/CMakeLists.txt b/core/libs/prx/libSceNpScoreRanking/CMakeLists.txt new file mode 100644 index 0000000000..d73264a441 --- /dev/null +++ b/core/libs/prx/libSceNpScoreRanking/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpScoreRanking SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpScoreRanking PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpScoreRanking) + +if(BUILD_TESTING) + add_test_executable(guest_np_score_offline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpScoreOffline.cpp) + target_include_directories(guest_np_score_offline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_score_offline_tests PRIVATE libSceNpScoreRanking) + configure_windows_unwind(guest_np_score_offline_tests) + add_test(NAME guest_np_score_offline COMMAND guest_np_score_offline_tests) + set_tests_properties(guest_np_score_offline PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_score_offline PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpScoreRanking/Export.cpp b/core/libs/prx/libSceNpScoreRanking/Export.cpp index 1bb8bc6df6..47690794d0 100644 --- a/core/libs/prx/libSceNpScoreRanking/Export.cpp +++ b/core/libs/prx/libSceNpScoreRanking/Export.cpp @@ -1,81 +1,234 @@ +#include +#include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +static constexpr int SCE_NP_ERROR_SIGNED_OUT = static_cast(0x80550006); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT = static_cast(0x80550704); +static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS = static_cast(0x80550706); +static constexpr int SCE_NP_COMMUNITY_ERROR_ABORTED = static_cast(0x80550707); +static constexpr int SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT = static_cast(0x8055070C); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ID = static_cast(0x8055070E); +static constexpr int SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT = static_cast(0x80550714); +static constexpr int SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID = static_cast(0x80550719); +static constexpr std::uint32_t INVALID_SERVICE_LABEL = 0xFFFFFFFF; +static constexpr std::size_t MAX_OBJECTS = 32; +static constexpr std::uint64_t MAX_RANGE_PER_REQUEST = 100; +static constexpr std::uint64_t MAX_NPID_PER_REQUEST = 101; +static constexpr std::uint64_t GAME_INFO_MAX_SIZE = 189; +static constexpr std::uint64_t NP_ID_SIZE = 36; +static constexpr std::uint64_t RANK_DATA_SIZE = 0x80; +static constexpr std::uint64_t PLAYER_RANK_DATA_SIZE = 0x88; +static constexpr std::uint64_t COMMENT_SIZE = 64; +static constexpr std::uint64_t GAME_INFO_SIZE = 200; +static constexpr std::uint64_t FRIEND_RANKING_OPTION_SIZE = 24; +static constexpr int POLL_FINISHED = 0; +static constexpr int POLL_RUNNING = 1; + +struct NpScoreGameInfo { + std::uint64_t infoSize; + std::uint8_t data[189]; + std::uint8_t pad[3]; +}; +static_assert(sizeof(NpScoreGameInfo) == GAME_INFO_SIZE); + +struct NpScoreFriendRankingOption { + std::uint64_t size; + const std::uint32_t* startSerialRank; + const std::uint32_t* hits; +}; +static_assert(sizeof(NpScoreFriendRankingOption) == FRIEND_RANKING_OPTION_SIZE); + +namespace { +struct Request { + int titleCtxId = 0; + std::optional result; +}; + +std::mutex scoreMutex; +std::unordered_set titleContexts; +std::unordered_map requests; +int nextTitleContext = 1; +int nextRequest = 1; + +int CheckArrays(std::uint64_t arrayNum, const void* commentArray, std::uint64_t commentArraySize, const void* infoArray, std::uint64_t infoArraySize) { + if (commentArray && commentArraySize != arrayNum * COMMENT_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (infoArray && infoArraySize != arrayNum * GAME_INFO_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + return 0; +} + +int Offline(int reqId) { + std::lock_guard lock(scoreMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (request->second.result == SCE_NP_COMMUNITY_ERROR_ABORTED) return SCE_NP_COMMUNITY_ERROR_ABORTED; + if (!request->second.result) request->second.result = SCE_NP_ERROR_SIGNED_OUT; + return SCE_NP_ERROR_SIGNED_OUT; +} + +int RecordScore(int reqId, const NpScoreGameInfo* gameInfo, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (gameInfo && gameInfo->infoSize > GAME_INFO_MAX_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + return Offline(reqId); +} + +int GetRankingByRange(int reqId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayNum > MAX_RANGE_PER_REQUEST) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (startSerialRank == 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!rankArray || arrayNum == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (rankArraySize != arrayNum * RANK_DATA_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (const int error = CheckArrays(arrayNum, commentArray, commentArraySize, infoArray, infoArraySize)) return error; + return Offline(reqId); +} + +int GetRankingByNpId(int reqId, const void* npIdArray, std::uint64_t npIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* option) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayNum > MAX_NPID_PER_REQUEST) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_NPID; + if (!npIdArray || !rankArray) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (npIdArraySize != arrayNum * NP_ID_SIZE || rankArraySize != arrayNum * PLAYER_RANK_DATA_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + if (const int error = CheckArrays(arrayNum, commentArray, commentArraySize, infoArray, infoArraySize)) return error; + return Offline(reqId); +} + +int GetFriendsRanking(int reqId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, const NpScoreFriendRankingOption* option) { + if (!rankArray) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (rankArraySize != arrayNum * RANK_DATA_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ALIGNMENT; + std::uint32_t startSerialRank = 1; + if (includeSelf == 0) { + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + } else if (option) { + if (option->size != FRIEND_RANKING_OPTION_SIZE) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (option->startSerialRank) startSerialRank = *option->startSerialRank; + } + if (arrayNum > MAX_RANGE_PER_REQUEST) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (startSerialRank == 0) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (arrayNum == 0) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + if (const int error = CheckArrays(arrayNum, commentArray, commentArraySize, infoArray, infoArraySize)) return error; + return Offline(reqId); +} +} + extern "C" { -int APS5_VABI sceNpScoreAbortRequest() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpScoreAbortRequest(int reqId) { + std::lock_guard lock(scoreMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.result) request->second.result = SCE_NP_COMMUNITY_ERROR_ABORTED; return 0; } -int APS5_VABI sceNpScoreCreateNpTitleCtx() { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceNpScoreCreateNpTitleCtx(std::uint32_t serviceLabel, const void* selfNpId) { + if (serviceLabel == INVALID_SERVICE_LABEL) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!selfNpId) return SCE_NP_COMMUNITY_ERROR_INSUFFICIENT_ARGUMENT; + std::lock_guard lock(scoreMutex); + if (titleContexts.size() >= MAX_OBJECTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int titleCtxId = nextTitleContext++; + titleContexts.insert(titleCtxId); + return titleCtxId; +} + +int APS5_VABI sceNpScoreCreateRequest(int titleCtxId) { + std::lock_guard lock(scoreMutex); + if (!titleContexts.contains(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (requests.size() >= MAX_OBJECTS) return SCE_NP_COMMUNITY_ERROR_TOO_MANY_OBJECTS; + const int reqId = nextRequest++; + requests.emplace(reqId, Request{titleCtxId}); + return reqId; +} + +int APS5_VABI sceNpScoreDeleteNpTitleCtx(int titleCtxId) { + std::lock_guard lock(scoreMutex); + if (!titleContexts.erase(titleCtxId)) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + std::erase_if(requests, [titleCtxId](const auto& entry) { return entry.second.titleCtxId == titleCtxId; }); return 0; } -int APS5_VABI sceNpScoreCreateRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreDeleteRequest(int reqId) { + std::lock_guard lock(scoreMutex); + return requests.erase(reqId) ? 0 : SCE_NP_COMMUNITY_ERROR_INVALID_ID; } -int APS5_VABI sceNpScoreDeleteNpTitleCtx() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetBoardInfo(int reqId, std::uint32_t boardId, void* boardInfo, void* option) { + (void)boardId; + if (option) return SCE_NP_COMMUNITY_ERROR_INVALID_ARGUMENT; + if (!boardInfo) throw std::runtime_error("sceNpScoreGetBoardInfo: the answer for a null board info depends on the title's SDK version"); + return Offline(reqId); } -int APS5_VABI sceNpScoreDeleteRequest() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetFriendsRanking(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetFriendsRanking(reqId, includeSelf, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetBoardInfo() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetFriendsRankingAsync(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const NpScoreFriendRankingOption* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetFriendsRanking(reqId, includeSelf, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetFriendsRanking() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetRankingByNpId(int reqId, std::uint32_t boardId, const void* npIdArray, std::uint64_t npIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByNpId(reqId, npIdArray, npIdArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetFriendsRankingAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetRankingByNpIdAsync(int reqId, std::uint32_t boardId, const void* npIdArray, std::uint64_t npIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByNpId(reqId, npIdArray, npIdArraySize, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetRankingByNpId() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetRankingByRange(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByRange(reqId, startSerialRank, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetRankingByNpIdAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreGetRankingByRangeAsync(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option) { + (void)boardId; + (void)lastSortDate; + (void)totalRecord; + return GetRankingByRange(reqId, startSerialRank, rankArray, rankArraySize, commentArray, commentArraySize, infoArray, infoArraySize, arrayNum, option); } -int APS5_VABI sceNpScoreGetRankingByRange() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScorePollAsync(int reqId, int* result) { + std::lock_guard lock(scoreMutex); + const auto request = requests.find(reqId); + if (request == requests.end()) return SCE_NP_COMMUNITY_ERROR_INVALID_ID; + if (!request->second.result) return POLL_RUNNING; + if (result) *result = *request->second.result; + return POLL_FINISHED; } -int APS5_VABI sceNpScoreGetRankingByRangeAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreRecordScore(int reqId, std::uint32_t boardId, std::int64_t score, const void* scoreComment, const NpScoreGameInfo* gameInfo, std::uint32_t* tmpRank, const void* compareDate, void* option) { + (void)boardId; + (void)score; + (void)scoreComment; + (void)tmpRank; + (void)compareDate; + return RecordScore(reqId, gameInfo, option); } -int APS5_VABI sceNpScorePollAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreRecordScore() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceNpScoreRecordScoreAsync() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceNpScoreRecordScoreAsync(int reqId, std::uint32_t boardId, std::int64_t score, const void* scoreComment, const NpScoreGameInfo* gameInfo, std::uint32_t* tmpRank, const void* compareDate, void* option) { + (void)boardId; + (void)score; + (void)scoreComment; + (void)tmpRank; + (void)compareDate; + return RecordScore(reqId, gameInfo, option); } } diff --git a/core/libs/tests/GuestNpScoreOffline.cpp b/core/libs/tests/GuestNpScoreOffline.cpp new file mode 100644 index 0000000000..cb6adb87eb --- /dev/null +++ b/core/libs/tests/GuestNpScoreOffline.cpp @@ -0,0 +1,223 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpScoreCreateNpTitleCtx(std::uint32_t serviceLabel, const void* selfNpId); +int APS5_VABI sceNpScoreDeleteNpTitleCtx(int titleCtxId); +int APS5_VABI sceNpScoreCreateRequest(int titleCtxId); +int APS5_VABI sceNpScoreDeleteRequest(int reqId); +int APS5_VABI sceNpScoreAbortRequest(int reqId); +int APS5_VABI sceNpScorePollAsync(int reqId, int* result); +int APS5_VABI sceNpScoreRecordScore(int reqId, std::uint32_t boardId, std::int64_t score, const void* scoreComment, const void* gameInfo, std::uint32_t* tmpRank, const void* compareDate, void* option); +int APS5_VABI sceNpScoreRecordScoreAsync(int reqId, std::uint32_t boardId, std::int64_t score, const void* scoreComment, const void* gameInfo, std::uint32_t* tmpRank, const void* compareDate, void* option); +int APS5_VABI sceNpScoreGetRankingByRange(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByRangeAsync(int reqId, std::uint32_t boardId, std::uint32_t startSerialRank, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByNpId(int reqId, std::uint32_t boardId, const void* npIdArray, std::uint64_t npIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetRankingByNpIdAsync(int reqId, std::uint32_t boardId, const void* npIdArray, std::uint64_t npIdArraySize, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, void* option); +int APS5_VABI sceNpScoreGetFriendsRanking(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const void* option); +int APS5_VABI sceNpScoreGetFriendsRankingAsync(int reqId, std::uint32_t boardId, int includeSelf, void* rankArray, std::uint64_t rankArraySize, void* commentArray, std::uint64_t commentArraySize, void* infoArray, std::uint64_t infoArraySize, std::uint64_t arrayNum, void* lastSortDate, void* totalRecord, const void* option); +int APS5_VABI sceNpScoreGetBoardInfo(int reqId, std::uint32_t boardId, void* boardInfo, void* option); +} + +namespace { + +constexpr int SignedOut = static_cast(0x80550006u); +constexpr int InvalidArgument = static_cast(0x80550704u); +constexpr int TooManyObjects = static_cast(0x80550706u); +constexpr int Aborted = static_cast(0x80550707u); +constexpr int InsufficientArgument = static_cast(0x8055070Cu); +constexpr int InvalidId = static_cast(0x8055070Eu); +constexpr int InvalidAlignment = static_cast(0x80550714u); +constexpr int TooManyNpId = static_cast(0x80550719u); +constexpr int PollFinished = 0; +constexpr int PollRunning = 1; + +struct GameInfo { + std::uint64_t infoSize; + std::uint8_t data[189]; + std::uint8_t pad[3]; +}; + +struct FriendOption { + std::uint64_t size; + const std::uint32_t* startSerialRank; + const std::uint32_t* hits; +}; + +int failures = 0; +int context = 0; +unsigned char selfNpId[36]; +unsigned char npIds[2 * 36]; +unsigned char ranks[2 * 0x88]; +unsigned char comments[2 * 64]; +unsigned char infos[2 * 200]; +unsigned char board[24]; +unsigned char manyNpIds[101 * 36]; +unsigned char manyRanks[101 * 0x88]; + +void Require(bool value, const char* name, const char* what) { + if (value) return; + std::fprintf(stderr, "%s: %s\n", name, what); + ++failures; +} + +int NewRequest() { + return sceNpScoreCreateRequest(context); +} + +bool Untouched() { + for (const unsigned char value : ranks) if (value != 0xA5) return false; + for (const unsigned char value : comments) if (value != 0xA5) return false; + for (const unsigned char value : infos) if (value != 0xA5) return false; + for (const unsigned char value : board) if (value != 0xA5) return false; + return true; +} + +void CheckOffline(const char* name, int status, int req) { + Require(status == SignedOut, name, "valid arguments did not return SIGNED_OUT"); + int result = 0; + Require(sceNpScorePollAsync(req, &result) == PollFinished && result == SignedOut, name, "the request did not finish with SIGNED_OUT"); +} + +void CheckContexts() { + Require(sceNpScoreCreateNpTitleCtx(0xFFFFFFFFu, nullptr) == InvalidArgument, "sceNpScoreCreateNpTitleCtx", "the invalid service label was not refused before the null NpId"); + Require(sceNpScoreCreateNpTitleCtx(0, nullptr) == InsufficientArgument, "sceNpScoreCreateNpTitleCtx", "a null NpId was taken"); + context = sceNpScoreCreateNpTitleCtx(0, selfNpId); + Require(context == 1, "sceNpScoreCreateNpTitleCtx", "the first context is not id 1"); + Require(sceNpScoreCreateRequest(context + 100) == InvalidId, "sceNpScoreCreateRequest", "an unknown context was taken"); + int extra[31]; + for (int& entry : extra) entry = sceNpScoreCreateNpTitleCtx(1, selfNpId); + Require(extra[30] == 32, "sceNpScoreCreateNpTitleCtx", "32 contexts could not be created"); + Require(sceNpScoreCreateNpTitleCtx(1, selfNpId) == TooManyObjects, "sceNpScoreCreateNpTitleCtx", "a 33rd context did not return TOO_MANY_OBJECTS"); + const int kept = sceNpScoreCreateRequest(extra[0]); + Require(kept == 1, "sceNpScoreCreateRequest", "the first request is not id 1"); + for (const int entry : extra) Require(sceNpScoreDeleteNpTitleCtx(entry) == 0, "sceNpScoreDeleteNpTitleCtx", "deleting a context failed"); + Require(sceNpScoreDeleteRequest(kept) == InvalidId, "sceNpScoreDeleteNpTitleCtx", "a request of a deleted context was kept"); + Require(sceNpScoreDeleteNpTitleCtx(extra[0]) == InvalidId, "sceNpScoreDeleteNpTitleCtx", "a deleted context was found"); + int requests[32]; + for (int& entry : requests) entry = NewRequest(); + Require(requests[31] > 0, "sceNpScoreCreateRequest", "32 requests could not be created"); + Require(NewRequest() == TooManyObjects, "sceNpScoreCreateRequest", "a 33rd request did not return TOO_MANY_OBJECTS"); + for (const int entry : requests) sceNpScoreDeleteRequest(entry); + Require(sceNpScoreDeleteRequest(requests[0]) == InvalidId, "sceNpScoreDeleteRequest", "a deleted request was found"); +} + +void CheckRequests() { + int req = NewRequest(); + int result = 7; + Require(sceNpScorePollAsync(req + 100, &result) == InvalidId, "sceNpScorePollAsync", "an unknown request was taken"); + Require(sceNpScorePollAsync(req, &result) == PollRunning && result == 7, "sceNpScorePollAsync", "an unstarted request was not running"); + Require(sceNpScoreAbortRequest(req + 100) == InvalidId, "sceNpScoreAbortRequest", "an unknown request was taken"); + Require(sceNpScoreAbortRequest(req) == 0, "sceNpScoreAbortRequest", "aborting failed"); + Require(sceNpScorePollAsync(req, nullptr) == PollFinished, "sceNpScorePollAsync", "a null result was not accepted"); + Require(sceNpScorePollAsync(req, &result) == PollFinished && result == Aborted, "sceNpScoreAbortRequest", "an aborted request did not finish with ABORTED"); + Require(sceNpScoreRecordScore(req, 1, 5, nullptr, nullptr, nullptr, nullptr, nullptr) == Aborted, "sceNpScoreRecordScore", "an aborted request did not return ABORTED"); + sceNpScoreDeleteRequest(req); + + req = NewRequest(); + Require(sceNpScoreRecordScore(req, 1, 5, nullptr, nullptr, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreRecordScore", "the first call did not return SIGNED_OUT"); + Require(sceNpScoreAbortRequest(req) == 0, "sceNpScoreAbortRequest", "aborting a finished request failed"); + Require(sceNpScorePollAsync(req, &result) == PollFinished && result == SignedOut, "sceNpScoreAbortRequest", "aborting a finished request changed its result"); + Require(sceNpScoreRecordScore(req, 1, 5, nullptr, nullptr, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreRecordScore", "a finished request was not reused"); + sceNpScoreDeleteRequest(req); +} + +void CheckRecord() { + GameInfo info{}; + std::uint32_t tmpRank = 77; + const int req = NewRequest(); + info.infoSize = 190; + Require(sceNpScoreRecordScore(req, 1, 5, nullptr, &info, &tmpRank, nullptr, &info) == InvalidArgument, "sceNpScoreRecordScore", "an option was taken"); + Require(sceNpScoreRecordScore(req, 1, 5, nullptr, &info, &tmpRank, nullptr, nullptr) == InvalidArgument, "sceNpScoreRecordScore", "190 bytes of game info were taken"); + Require(sceNpScoreRecordScoreAsync(req, 1, 5, nullptr, &info, &tmpRank, nullptr, nullptr) == InvalidArgument, "sceNpScoreRecordScoreAsync", "190 bytes of game info were taken"); + info.infoSize = 189; + Require(sceNpScoreRecordScore(req + 100, 1, 5, nullptr, &info, &tmpRank, nullptr, nullptr) == InvalidId, "sceNpScoreRecordScore", "an unknown request was taken"); + Require(sceNpScorePollAsync(req, nullptr) == PollRunning, "sceNpScoreRecordScore", "a refused call finished the request"); + CheckOffline("sceNpScoreRecordScoreAsync", sceNpScoreRecordScoreAsync(req, 1, 5, nullptr, &info, &tmpRank, nullptr, nullptr), req); + Require(tmpRank == 77, "sceNpScoreRecordScoreAsync", "a rank was written"); + sceNpScoreDeleteRequest(req); +} + +void CheckRange() { + const int req = NewRequest(); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, board) == InvalidArgument, "sceNpScoreGetRankingByRange", "an option was taken"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 101 * 0x80, nullptr, 0, nullptr, 0, 101, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetRankingByRange", "101 ranks were taken"); + Require(sceNpScoreGetRankingByRange(req, 1, 0, nullptr, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetRankingByRange", "rank 0 was not refused before the null array"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, nullptr, 3, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByRange", "a null rank array was not refused before its size"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0, nullptr, 0, nullptr, 0, 0, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByRange", "zero ranks were taken"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByRange", "a rank array size other than 0x80 per rank was taken"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0x100, comments, 64, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByRange", "a comment array size other than 64 per rank was taken"); + Require(sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0x100, comments, 128, infos, 200, 2, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByRange", "an info array size other than 200 per rank was taken"); + Require(sceNpScoreGetRankingByRange(req + 100, 1, 1, ranks, 0x100, comments, 128, infos, 400, 2, nullptr, nullptr, nullptr) == InvalidId, "sceNpScoreGetRankingByRange", "an unknown request was taken"); + CheckOffline("sceNpScoreGetRankingByRange", sceNpScoreGetRankingByRange(req, 1, 1, ranks, 0x100, comments, 128, infos, 400, 2, nullptr, nullptr, nullptr), req); + Require(sceNpScoreGetRankingByRangeAsync(req, 1, 1, ranks, 0x100, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByRangeAsync", "valid arguments did not return SIGNED_OUT"); + Require(sceNpScoreGetRankingByRangeAsync(req, 1, 0, ranks, 0x100, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetRankingByRangeAsync", "rank 0 was taken"); + Require(sceNpScoreGetRankingByRangeAsync(req, 1, 1, manyRanks, 100 * 0x80, nullptr, 0, nullptr, 0, 100, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByRangeAsync", "100 ranks did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); +} + +void CheckNpId() { + const int req = NewRequest(); + Require(sceNpScoreGetRankingByNpId(req, 1, npIds, 36, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, board) == InvalidArgument, "sceNpScoreGetRankingByNpId", "an option was taken"); + Require(sceNpScoreGetRankingByNpId(req, 1, nullptr, 0, nullptr, 0, nullptr, 0, nullptr, 0, 102, nullptr, nullptr, nullptr) == TooManyNpId, "sceNpScoreGetRankingByNpId", "102 NpIds were not refused before the null arrays"); + Require(sceNpScoreGetRankingByNpId(req, 1, nullptr, 36, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByNpId", "a null NpId array was taken"); + Require(sceNpScoreGetRankingByNpId(req, 1, npIds, 36, nullptr, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetRankingByNpId", "a null rank array was taken"); + Require(sceNpScoreGetRankingByNpId(req, 1, npIds, 24, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByNpId", "an NpId array size other than 36 per NpId was taken"); + Require(sceNpScoreGetRankingByNpId(req, 1, npIds, 36, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByNpId", "a rank array size other than 0x88 per NpId was taken"); + Require(sceNpScoreGetRankingByNpId(req, 1, npIds, 36, ranks, 0x88, comments, 128, nullptr, 0, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetRankingByNpId", "a comment array size other than 64 per NpId was taken"); + CheckOffline("sceNpScoreGetRankingByNpId", sceNpScoreGetRankingByNpId(req, 1, npIds, 72, ranks, 0x110, comments, 128, infos, 400, 2, nullptr, nullptr, nullptr), req); + Require(sceNpScoreGetRankingByNpIdAsync(req, 1, npIds, 0, ranks, 0, nullptr, 0, nullptr, 0, 0, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByNpIdAsync", "zero NpIds did not return SIGNED_OUT"); + Require(sceNpScoreGetRankingByNpIdAsync(req, 1, manyNpIds, sizeof(manyNpIds), manyRanks, sizeof(manyRanks), nullptr, 0, nullptr, 0, 101, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetRankingByNpIdAsync", "101 NpIds did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); +} + +void CheckFriends() { + const int req = NewRequest(); + const std::uint32_t zero = 0; + const std::uint32_t five = 5; + const FriendOption shortOption{16, nullptr, nullptr}; + const FriendOption rankZero{24, &zero, nullptr}; + const FriendOption rankFive{24, &five, nullptr}; + Require(sceNpScoreGetFriendsRanking(req, 1, 0, nullptr, 0x80, nullptr, 0, nullptr, 0, 0, nullptr, nullptr, &shortOption) == InvalidArgument, "sceNpScoreGetFriendsRanking", "a null rank array was taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 0, ranks, 0x88, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, &shortOption) == InvalidAlignment, "sceNpScoreGetFriendsRanking", "the rank array size was not checked before the option"); + Require(sceNpScoreGetFriendsRanking(req, 1, 0, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, &rankFive) == InvalidArgument, "sceNpScoreGetFriendsRanking", "an option without includeSelf was taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, &shortOption) == InvalidArgument, "sceNpScoreGetFriendsRanking", "an option size other than 24 was taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 0x80, nullptr, 0, nullptr, 0, 1, nullptr, nullptr, &rankZero) == InvalidArgument, "sceNpScoreGetFriendsRanking", "start rank 0 was taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 101 * 0x80, nullptr, 0, nullptr, 0, 101, nullptr, nullptr, nullptr) == InvalidArgument, "sceNpScoreGetFriendsRanking", "101 friends were taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 0, nullptr, 0, nullptr, 0, 0, nullptr, nullptr, nullptr) == InsufficientArgument, "sceNpScoreGetFriendsRanking", "zero friends were taken"); + Require(sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 0x80, nullptr, 0, infos, 400, 1, nullptr, nullptr, nullptr) == InvalidAlignment, "sceNpScoreGetFriendsRanking", "an info array size other than 200 per friend was taken"); + CheckOffline("sceNpScoreGetFriendsRanking", sceNpScoreGetFriendsRanking(req, 1, 1, ranks, 0x80, comments, 64, infos, 200, 1, nullptr, nullptr, &rankFive), req); + Require(sceNpScoreGetFriendsRankingAsync(req, 1, 0, ranks, 0x100, nullptr, 0, nullptr, 0, 2, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetFriendsRankingAsync", "valid arguments did not return SIGNED_OUT"); + Require(sceNpScoreGetFriendsRankingAsync(req, 1, 0, manyRanks, 100 * 0x80, nullptr, 0, nullptr, 0, 100, nullptr, nullptr, nullptr) == SignedOut, "sceNpScoreGetFriendsRankingAsync", "100 friends did not return SIGNED_OUT"); + sceNpScoreDeleteRequest(req); +} + +void CheckBoard() { + const int req = NewRequest(); + Require(sceNpScoreGetBoardInfo(req, 1, board, board) == InvalidArgument, "sceNpScoreGetBoardInfo", "an option was taken"); + Require(sceNpScoreGetBoardInfo(req + 100, 1, board, nullptr) == InvalidId, "sceNpScoreGetBoardInfo", "an unknown request was taken"); + CheckOffline("sceNpScoreGetBoardInfo", sceNpScoreGetBoardInfo(req, 1, board, nullptr), req); + sceNpScoreDeleteRequest(req); +} + +} + +int main() { + std::memset(ranks, 0xA5, sizeof(ranks)); + std::memset(comments, 0xA5, sizeof(comments)); + std::memset(infos, 0xA5, sizeof(infos)); + std::memset(board, 0xA5, sizeof(board)); + CheckContexts(); + CheckRequests(); + CheckRecord(); + CheckRange(); + CheckNpId(); + CheckFriends(); + CheckBoard(); + Require(Untouched(), "libSceNpScoreRanking", "an output array was written"); + if (failures != 0) return 1; + std::puts("NpScoreRanking offline tests passed"); + return 0; +} diff --git a/docs/dev/debt/libSceNpScoreRanking.md b/docs/dev/debt/libSceNpScoreRanking.md new file mode 100644 index 0000000000..d373e91695 --- /dev/null +++ b/docs/dev/debt/libSceNpScoreRanking.md @@ -0,0 +1,5 @@ +# libSceNpScoreRanking technical debt + +### Functional + +- [libSceNpScoreRanking](../../../core/libs/prx/libSceNpScoreRanking/Export.cpp) (libSceNpScoreRanking) - offline only, not measured on a PS5: the argument checks, their order, the limit of 32 title contexts and 32 requests and the array sizes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_score/np_score.cpp#L64-L1210), and a valid call then returns `SCE_NP_ERROR_SIGNED_OUT` and finishes its request with it, as shadPS4 does when the user [has no connection](https://github.com/shadps4-emu/shadPS4/blob/91e6afa69b0a64b0afaa0281b80f8fbb0cd88136/src/core/libraries/np/np_handler/np_handler_score.cpp#L18-L26). No output is written, where shadPS4 first clears the rank, comment and info arrays. A null `boardInfo` throws: shadPS4 answers it by the title's SDK version From 1d0c48d1ee1324e9edb1dbea3c1ec29a668ceccc Mon Sep 17 00:00:00 2001 From: valenciana-nc Date: Tue, 20 Oct 2026 21:03:01 -0600 Subject: [PATCH 183/318] fix(libc): preserve exception flags for zero-argument float Bessel calls --- core/libs/prx/libc/src/Math.cpp | 1 - core/libs/tests/GuestBessel.cpp | 28 ++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 37616b7be3..39c9c405cc 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -226,7 +226,6 @@ T GuestFmax(T x, T y) { } float GuestBesselSecondKindZero() { - std::feraiseexcept(FE_DIVBYZERO); return -std::numeric_limits::infinity(); } diff --git a/core/libs/tests/GuestBessel.cpp b/core/libs/tests/GuestBessel.cpp index 75c94ec4ac..6650201428 100644 --- a/core/libs/tests/GuestBessel.cpp +++ b/core/libs/tests/GuestBessel.cpp @@ -17,6 +17,9 @@ double APS5_VABI jn_nid_postfix(int, double); double APS5_VABI y0_nid_postfix(double); double APS5_VABI y1_nid_postfix(double); double APS5_VABI yn_nid_postfix(int, double); +float APS5_VABI y0f_nid_postfix(float); +float APS5_VABI y1f_nid_postfix(float); +float APS5_VABI ynf_nid_postfix(int, float); int* APS5_VABI __error_nid_postfix(); } @@ -100,6 +103,30 @@ static void CheckSecondKind() { } } +static void CheckFloatSecondKindZero() { + const char* names[] = {"y0f", "y1f", "ynf"}; + for (int function = 0; function < 3; ++function) { + for (const int order : {INT_MIN, -3, -2, -1, 0, 1, 2, 3, INT_MAX}) { + for (const float input : {0.0f, -0.0f}) { + for (const int flags : {0, FE_INEXACT, FE_DIVBYZERO}) { + std::feclearexcept(FE_ALL_EXCEPT); + std::feraiseexcept(flags); + const int expectedFlags = std::fetestexcept(FE_ALL_EXCEPT); + *__error_nid_postfix() = 12345; + const float result = function == 0 ? y0f_nid_postfix(input) : function == 1 ? y1f_nid_postfix(input) : ynf_nid_postfix(order, input); + const int actualFlags = std::fetestexcept(FE_ALL_EXCEPT); + if (std::bit_cast(result) != 0xff800000u || actualFlags != expectedFlags || *__error_nid_postfix() != 12345) { + std::fprintf(stderr, "Guest %s(%d, %08x): result %08x, flags %x, expected %x, errno %d\n", names[function], order, + std::bit_cast(input), std::bit_cast(result), actualFlags, expectedFlags, *__error_nid_postfix()); + std::abort(); + } + } + } + } + } + std::feclearexcept(FE_ALL_EXCEPT); +} + static void CheckIntegerOrders() { const OrderCase first[] = { {2, 0x3ddb7cdfd9d7bdbbull, 0x3b979ca10c924224ull}, {5, 0xbddb7cdfd9d7bdbbull, 0xb4cfed4f3ec0c466ull}, {33, 0x3e00000000000000ull, 0x0000000000000000ull}, @@ -181,6 +208,7 @@ static void CheckValues() { int main() { CheckFirstKind(); CheckSecondKind(); + CheckFloatSecondKindZero(); CheckIntegerOrders(); CheckValues(); return 0; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c9cda8fa5a..8092690d8d 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -89,7 +89,7 @@ - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code - [atanhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `atanhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_atanhf.c`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_atanhf.c) calling msun's [`log1pf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_log1pf.c), whose paths for the positive arguments it passes are inlined here; the exported `log1pf` is still the host's. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, and for |x| = 1 it raises `FE_DIVBYZERO` and sets `errno` to `ERANGE` before returning `x / 0`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits, `errno` and floating-point exceptions as this implementation for all 2^32 arguments; it was not run on Linux. That the system `libSceLibcInternal`, which Dusklight (PPSA99305) imports while bundling a zero-return `libc.prx`, has the same code is assumed -- [j0, j1, jn, y0, y1 and yn](../../../core/libs/prx/libc/src/Bessel.cpp) (libc) call the host's `sin`, `cos`, `sincos` and `log`, so the last bits can differ from the console. The rest follows the system `libc.prx` built as `J03093718` (disassembly), which given the console's own `sin`, `cos`, `sincos` and `log` gave the same bits and exception flags for 90 million arguments: msun's [`e_j0.c`, `e_j1.c` and `e_jn.c`](https://github.com/freebsd/freebsd-src/tree/release/10.0.0/lib/msun/src) with each multiply-add the console fuses done as one FMA. As in that release, before FreeBSD commit `186f620727`, `y0`, `y1` and `yn` return `-inf` for ±0 and `0x7ff8000000000000` for a negative argument without raising an exception. The console's `y0f`, `y1f` and `ynf` raise none either, unlike [ours](../../../core/libs/prx/libc/src/Math.cpp), which raise `FE_DIVBYZERO` at ±0. The order wraps at 32 bits as on the console: `jn(1 << 30, x)` never returns for 2^-29 <= |x| < 2^30 +- [j0, j1, jn, y0, y1 and yn](../../../core/libs/prx/libc/src/Bessel.cpp) (libc) call the host's `sin`, `cos`, `sincos` and `log`, so the last bits can differ from the console. The rest follows the system `libc.prx` built as `J03093718` (disassembly), which given the console's own `sin`, `cos`, `sincos` and `log` gave the same bits and exception flags for 90 million arguments: msun's [`e_j0.c`, `e_j1.c` and `e_jn.c`](https://github.com/freebsd/freebsd-src/tree/release/10.0.0/lib/msun/src) with each multiply-add the console fuses done as one FMA. As in that release, before FreeBSD commit `186f620727`, `y0`, `y1` and `yn` return `-inf` for ±0 and `0x7ff8000000000000` for a negative argument without raising an exception. The order wraps at 32 bits as on the console: `jn(1 << 30, x)` never returns for 2^-29 <= |x| < 2^30 - [getenv, setenv, unsetenv, putenv](../../../core/libs/prx/libc/src/Environment.cpp) (libc) keep one guest environment that starts empty, like the `environ` libkernel exports, and never reads or changes the host's. A title sees only the variables it sets itself; host variables such as `HOME` and `PATH` name host paths a guest cannot use (PS5MC, PPSA99420, exits with `XBMC_Run returned -1` when it gets the host's `HOME`). Whether the console starts a title with any variables set is not measured - [mergesort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) checks its arguments like the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x762D0): an element size under 4 sets `errno` to `EINVAL` (22) and returns -1, an empty array returns 0, and a failed `malloc` of the count times the size plus 8 bytes returns -1. The sort is a stable bottom-up merge, not FreeBSD's natural merge with its insertion sort for short runs, so the comparator is called in a different order and number of times; the result is the same for a consistent comparator. A null base or comparator and a size product that overflows, which the console does not check, throw - On macOS the [write watch](../../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: From 35b22f0ef38f07133818a72ef9b236fcbbbb110a Mon Sep 17 00:00:00 2001 From: valenciana-nc Date: Tue, 20 Oct 2026 10:28:00 -0600 Subject: [PATCH 184/318] fix(relinker): preserve icon and title metadata inputs --- core/relinker/main.cpp | 9 +- .../relinker/tests/test_output_collisions.py | 90 ++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 98bca27396..91c5b09ee7 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -337,10 +337,17 @@ int Run(const int argc, char* argv[]) { if (!manifestPath.empty()) manifestText = Relinker::GamePatch::WriteManifest(appliedPatches, mainTarget, Io::Utf8Path(patchFile), executables.front().LinuxBase); const Elfpatcher::Linux::LinuxDesktopEntryWriter desktopEntryWriter; - const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(Io::NativePath(args.inputPath).parent_path() / "sce_sys", Io::NativePath(absPath)); + const auto metadataRoot = Io::NativePath(args.inputPath).parent_path() / "sce_sys"; + const auto desktopEntry = args.toWindows || args.toMacos ? std::nullopt : desktopEntryWriter.Prepare(metadataRoot, Io::NativePath(absPath)); + std::vector metadataInputs; + if (args.toWindows || desktopEntry) metadataInputs.push_back(metadataRoot / "icon0.png"); + if (desktopEntry) metadataInputs.push_back(metadataRoot / "param.json"); const auto checkOutputInputs = [&](const std::filesystem::path& path, const char* kind) { if (!std::filesystem::exists(path)) return; + for (const auto& input : metadataInputs) + if (std::filesystem::exists(input) && SameOutputFile(input, path)) + throw Domain::RelinkerException(std::string(kind) + " output would overwrite input metadata: " + Io::Utf8Path(path)); for (const auto& executable : executables) if (SameOutputFile(Io::NativePath(executable.InputPath), path)) throw Domain::RelinkerException(std::string(kind) + " output would overwrite the input executable: " + Io::Utf8Path(path)); diff --git a/core/relinker/relinker/tests/test_output_collisions.py b/core/relinker/relinker/tests/test_output_collisions.py index 4213674c46..0361c30544 100644 --- a/core/relinker/relinker/tests/test_output_collisions.py +++ b/core/relinker/relinker/tests/test_output_collisions.py @@ -50,6 +50,93 @@ def case_sensitive(directory): return sensitive +def metadata_case(case, windows): + command, outputs, inputs = prepare(case, windows) + metadata = case / "sce_sys" + metadata.mkdir(exist_ok=True) + icon = metadata / "icon0.png" + param = metadata / "param.json" + icon.write_bytes(png(1, 1)) + param.write_text(json.dumps({"titleId": "PPSA00000", "defaultLanguage": "en-US", + "localizedParameters": {"en-US": {"titleName": "Sample Title"}}}), encoding="utf-8") + for path in outputs.values(): + path.write_bytes(b"previous output") + inputs.update({icon: icon.read_bytes(), param: param.read_bytes()}) + return command, outputs, inputs + + +def tree_snapshot(root): + return {path.relative_to(root): ("link", os.readlink(path)) if path.is_symlink() + else ("directory", None) if path.is_dir() else ("file", path.read_bytes()) + for path in root.rglob("*")} + + +def metadata_inputs(relinker, work): + checked = 0 + + def rejected(case, command): + before = tree_snapshot(case) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, encoding="utf-8", timeout=20) + assert result.returncode == 2 and "output would overwrite input metadata" in result.stderr, ( + case.name, result.returncode, result.stdout, result.stderr) + assert tree_snapshot(case) == before, case + + for windows in (False, True): + for name in (("icon0.png",) if windows else ("icon0.png", "param.json")): + case = work / f"metadata-direct-{windows}-{name}" + command, _, _ = metadata_case(case, windows) + rejected(case, [*command[:-1], str(case / "sce_sys" / name)]) + checked += 1 + outputs = ("executable", "module", "registry", "module-registry", "needed", "module-needed") + outputs += ("diagnostics",) if windows else ("icon", "desktop") + for alias in ("hardlink", "symlink"): + for output in outputs: + case = work / f"metadata-{windows}-{name}-{alias}-{output}" + command, paths, _ = metadata_case(case, windows) + destination = paths[output] + destination.unlink() + metadata = case / "sce_sys" / name + if alias == "hardlink": + os.link(metadata, destination) + else: + try: + destination.symlink_to(metadata) + except (OSError, NotImplementedError) as error: + print(f"Metadata symlink case skipped: {error}") + continue + rejected(case, command) + checked += 1 + case = work / f"metadata-distinct-{windows}" + command, paths, inputs = metadata_case(case, windows) + result = subprocess.run([str(relinker), *command], cwd=case, + capture_output=True, text=True, encoding="utf-8", timeout=20) + assert result.returncode == 0, (result.stdout, result.stderr) + for path, original in inputs.items(): + assert path.read_bytes() == original, path + assert paths["executable"].read_bytes().startswith(b"MZ" if windows else b"\x7fELF") + + for format, name in (("windows", "param.json"), ("macos", "icon0.png"), ("linux-no-icon", "param.json")): + case = work / f"metadata-unused-{format}" + command, _, inputs = metadata_case(case, format == "windows") + if format == "macos": + command.insert(0, "--macos") + if format == "linux-no-icon": + icon = case / "sce_sys/icon0.png" + icon.unlink() + inputs.pop(icon) + output = case / "sce_sys" / name + result = subprocess.run([str(relinker), *command[:-1], str(output)], cwd=case, + capture_output=True, text=True, encoding="utf-8", timeout=20) + assert result.returncode == 0, (format, result.stdout, result.stderr) + magic = b"MZ" if format == "windows" else b"\xcf\xfa\xed\xfe" if format == "macos" else b"\x7fELF" + assert output.read_bytes().startswith(magic), output + for path, original in inputs.items(): + if path != output: + assert path.read_bytes() == original, path + return checked + + def filesystem_aliases(relinker, work): if os.name == "nt": return @@ -91,6 +178,7 @@ def main(): checked = 0 with tempfile.TemporaryDirectory(prefix="anyps5-output-collisions-") as directory: work = Path(directory) + metadata_checked = metadata_inputs(relinker, work) filesystem_aliases(relinker, work) for windows, existing in itertools.product((False, True), repeat=2): case = work / f"needed-secondary-{windows}-{existing}" @@ -213,7 +301,7 @@ def main(): assert not list(case.rglob(".anyps5-output-identity-*")) finally: output.chmod(stat.S_IWRITE) - print(f"Output collision tests passed: {checked} hard-link pairs, direct paths, and identity controls") + print(f"Output collision tests passed: {checked} hard-link pairs, {metadata_checked} metadata protections, direct paths, and identity controls") if __name__ == "__main__": From 2c92b0f662369df6c0ec88eccdceea7f9a6e045a Mon Sep 17 00:00:00 2001 From: orezraey Date: Thu, 22 Oct 2026 21:26:24 -0300 Subject: [PATCH 185/318] fix(libkernel): refuse closing the standard descriptors with EPERM as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/File/src/Open.cpp | 2 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 +- core/libs/tests/GuestHostStreams.cpp | 10 ++++++++-- docs/dev/debt/libkernel.md | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 417c79bd96..2cb36ef96c 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -298,7 +298,7 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std } int APS5_VABI sceKernelClose(int d) { - if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EBADF; + if (File::IsHostStream(d)) return SCE_KERNEL_ERROR_EPERM; if (GuestSockets::IsOpen(d)) return GuestSockets::Close(d) == 0 ? 0 : SceKernelError(*__error_nid_postfix()); #ifdef _WIN32 const auto positioned = File::LockPositionedTransfer(d); diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index fa57cf492d..eef3f0b642 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -711,7 +711,7 @@ int APS5_VABI nmount_nid_postfix(const KernelIovec* iov, unsigned int niov, int } int APS5_VABI close_nid_postfix(int d) { - if (File::IsHostStream(d)) return PosixFailure(GUEST_EBADF); + if (File::IsHostStream(d)) return PosixFailure(GUEST_EPERM); int socketError = 0; if (KernelCloseSocket_nid_no_patch(d, &socketError)) { if (socketError == 0) return 0; diff --git a/core/libs/tests/GuestHostStreams.cpp b/core/libs/tests/GuestHostStreams.cpp index e31c1dad51..83a39b5768 100644 --- a/core/libs/tests/GuestHostStreams.cpp +++ b/core/libs/tests/GuestHostStreams.cpp @@ -11,7 +11,9 @@ int* APS5_VABI __error_nid_postfix(); static void Require(bool value) { if (!value) std::abort(); } +constexpr int GuestEperm = 1; constexpr int GuestEbadf = 9; +constexpr int SceKernelErrorEperm = static_cast(0x80020001); constexpr int SceKernelErrorEbadf = static_cast(0x80020009); static bool Open(int fd) { @@ -26,13 +28,17 @@ int main() { for (int fd = 0; fd < 3; ++fd) open[fd] = Open(fd); for (int fd = 0; fd < 3; ++fd) { if (!open[fd]) continue; - Require(close_nid_postfix(fd) == -1 && *__error_nid_postfix() == GuestEbadf); - Require(sceKernelClose(fd) == SceKernelErrorEbadf); + Require(close_nid_postfix(fd) == -1 && *__error_nid_postfix() == GuestEperm); + Require(sceKernelClose(fd) == SceKernelErrorEperm); Require(Open(fd)); } int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); for (const int fd : pipe) Require(fd >= 3 || !open[fd]); Require(close_nid_postfix(pipe[0]) == 0 && close_nid_postfix(pipe[1]) == 0); + Require(close_nid_postfix(pipe[0]) == -1 && *__error_nid_postfix() == GuestEbadf); + Require(sceKernelClose(pipe[1]) == SceKernelErrorEbadf); + Require(close_nid_postfix(-1) == -1 && *__error_nid_postfix() == GuestEbadf); + Require(sceKernelClose(-1) == SceKernelErrorEbadf); return 0; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c9f6ad57e9..847205b362 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -113,7 +113,7 @@ - [sceKernelGetCpuTemperature, sceKernelGetSocSensorTemperature](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - read the host's thermal sensors: on Linux from `/sys/class/hwmon` and `/sys/class/thermal`, on Windows from the ACPI thermal zones (`MSAcpi_ThermalZoneTemperature` over WMI) and the storage temperature property of the first physical drive. Sensor 0 is the CPU/package value, 1 the first other value and 2 an NVMe value; a host that exposes none reports every sensor as unsupported (`EINVAL`). The console's SoC channel count and meanings are hardware-dependent and differ from the host's, the host sensors are not the console's, the ACPI reading is unavailable on many machines, and the storage temperature property is numbered 52 on Windows 10 2004 and later and 22 before it, so the mapping, the values and the unsupported-input error are unverified; the storage channel was checked against the drive's own SMART composite temperature on one NVMe host (`StorageDeviceTemperatureProperty`, 36 °C on a WD_BLACK SN7100). `sceKernelGetCpuTemperature` reports sensor 0. PPSA99058 and PPSA99420 call both - [LinuxProtFromSce](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits - [getdtablesize](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - returns the smaller of the `RLIMIT_NOFILE` soft limit of `getrlimit` and the host's `getdtablesize()`, capped at `INT_MAX`: FreeBSD returns the smaller of the `RLIMIT_NOFILE` soft limit and `maxfilesperproc` ([`sys_getdtablesize`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L338-L355)), and the host call supplies that bound on macOS, where the soft limit alone is not one (`setrlimit` accepts soft limits above `maxfilesperproc`, `RLIM_INFINITY` included); `maxfilesperproc` is not measured on the console. The guest limit is the host's soft limit, raised to the hard limit when libkernel loads only on Linux: `core/libs/prx/libkernel/File/src/OpenFileLimit.cpp` is `__linux__`-only, so on macOS the guest gets whatever soft limit the host process inherited. VLC (PPSA85300) imports it for a `closefrom` loop in the child of its spawn helper ([libc_stubs.c](https://github.com/ZiZc3/VLC/blob/d46be6a68f4244466b041c2dc1b640915ac1a983/ps5/compat/libc_stubs.c#L66-L72)), which never runs because its `fork` fails with `ENOSYS` -- [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EBADF` and leaves it open. The console's result is unknown. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed +- [close, sceKernelClose](../../../core/libs/prx/libkernel/File/src/HostStreams.cpp) (libkernel) - descriptors 0 to 2 that are open when libkernel loads belong to the host process: closing one fails with `EPERM` (`sceKernelClose`: `0x80020001`) and leaves it open, as closing the open descriptors 0 to 2 of a payload process did on a PS5 (firmware 13.60); whether a title process can close them is unverified. PPSA21703 calls `sceKernelClose(0)` repeatedly; closing the host stream let the host C runtime give descriptor 0 to the next file opened, which the next call then closed - [sceKernelPrintBacktraceWithModuleInfo](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - imported by PS4 titles, which do not load without it, and added without an implementation: declared without parameters, every call throws ### Functional From 1f206b59c4da66d15a1c00dfff0896ca61b27722 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 22 Oct 2026 20:34:21 -0400 Subject: [PATCH 186/318] fix(libc): accept any setlocale category and only the C locale, as titles' libc.prx does setlocale returned a null pointer for a category outside 0 to 6 and accepted "POSIX" as a name for "C". The libc.prx that titles ship ignores the category, so setlocale(-1, NULL) and setlocale(7, "C") return "C", and returns a null pointer for "POSIX" as for any name other than "C" or the empty string. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/StdioExtras.cpp | 5 ++--- core/libs/tests/GuestCharacter.cpp | 13 +++++++++---- docs/dev/debt/libc.md | 1 + 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index 28b5f7b33b..fbbdd1a972 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -83,9 +83,8 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma } // Only the "C" locale exists. -const char* APS5_VABI setlocale_nid_postfix(int category, const char* locale) { - if (category < 0 || category > 6) return nullptr; - if (locale == nullptr || locale[0] == 0 || std::strcmp(locale, "C") == 0 || std::strcmp(locale, "POSIX") == 0) return "C"; +const char* APS5_VABI setlocale_nid_postfix(int, const char* locale) { + if (locale == nullptr || locale[0] == 0 || std::strcmp(locale, "C") == 0) return "C"; return nullptr; } diff --git a/core/libs/tests/GuestCharacter.cpp b/core/libs/tests/GuestCharacter.cpp index 7601716b77..94fa6665a1 100644 --- a/core/libs/tests/GuestCharacter.cpp +++ b/core/libs/tests/GuestCharacter.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include #include @@ -29,16 +30,20 @@ int main() { for (int category = 0; category <= 6; ++category) { const char* result = setlocale_nid_postfix(category, "C"); Require(result != nullptr && std::strcmp(result, "C") == 0); - result = setlocale_nid_postfix(category, "POSIX"); - Require(result != nullptr && std::strcmp(result, "C") == 0); + Require(setlocale_nid_postfix(category, "POSIX") == nullptr); result = setlocale_nid_postfix(category, nullptr); Require(result != nullptr && std::strcmp(result, "C") == 0); result = setlocale_nid_postfix(category, ""); Require(result != nullptr && std::strcmp(result, "C") == 0); Require(setlocale_nid_postfix(category, "en_US.UTF-8") == nullptr); } - Require(setlocale_nid_postfix(-1, "C") == nullptr); - Require(setlocale_nid_postfix(7, "C") == nullptr); + for (const int category : std::array{-1, 7, 100, 1 << 30}) { + const char* result = setlocale_nid_postfix(category, "C"); + Require(result != nullptr && std::strcmp(result, "C") == 0); + result = setlocale_nid_postfix(category, nullptr); + Require(result != nullptr && std::strcmp(result, "C") == 0); + Require(setlocale_nid_postfix(category, "POSIX") == nullptr && setlocale_nid_postfix(category, "x") == nullptr); + } for (int c = EOF; c <= 255; ++c) { Require(bool(isupper_nid_postfix(c)) == bool(std::isupper(c))); Require(bool(islower_nid_postfix(c)) == bool(std::islower(c))); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c9cda8fa5a..de5c794e5e 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -45,6 +45,7 @@ - [strndup and strdup](../../../core/libs/prx/libc/src/AllocatingStrings.cpp) (libc) return null with `errno` set to `EINVAL` (22) when `source` is null, preventing a host segfault on invalid guest input. POSIX.1-2008 and FreeBSD 11.4 libc leave `strndup(NULL)` and `strdup(NULL)` undefined (faulting); exact console behavior on hardware is unverified. - [snprintf_s and vsnprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - follow C11 K.3.5.3.5 and K.3.5.3.12 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`, as `snwprintf_s` does: a null buffer or format, `size` of 0 or above `RSIZE_MAX`, any `%n` and a null `%s`, `%ls` or `%S` argument return a negative value and set `buffer[0]` to zero when the buffer and size allow it; otherwise the output is truncated to the buffer and the full length is returned. A violation only returns: the constraint handler is not invoked, and the console's error value is unverified. Not confirmed on a PS5 title - [vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - returns the count of items converted before an undersized `%s`/`%c`/`%[` destination (0 when it is the first), not EOF. Not confirmed on a PS5 title +- [setlocale](../../../core/libs/prx/libc/src/StdioExtras.cpp) (libc) knows only the "C" locale, as the `libc.prx` that titles ship does: it ignores the category, returns "C" for a null, empty or "C" name and a null pointer for any other, "POSIX" included, without setting `errno`. Checked by running the copies bundled with PPSA07923 and PPSA13934 side by side for categories -40 to 79 and 2^30, not measured on a console. - [localeconv](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, re-exported by libSceAmpr) - the 0x90-byte Dinkumware struct (empty monetary strings, `CHAR_MAX` for the 14 `char` fields, `decimal_point` ".", `_False`/`_True` "false"/"true") is read from the `libc.prx` that Astro's Playroom ships. It is a fixed C-locale table: whether the console changes it with `setlocale` is not known, and `_Frac_grouping`, `_Frac_sep`, `_No`, `_Yes` are empty strings as in that dump - [set_constraint_handler_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc, re-exported by libSceAmpr) - the default handler is assumed to abort; each module keeps its own handler state, so a handler set through one library is not seen by the other. Within a module every `_s` function calls the installed handler, with the guest's calling convention. `ctime_s` and `asctime_s` follow the Dinkumware 26-byte format with `EINVAL` for a buffer under 26 bytes, a null pointer or an out-of-range weekday/month; `ctime_s` uses the guest time zone of `localtime_s`, and the console's exact checks are not verified - [qsort_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) sorts as `qsort` does and passes its context as the comparator's third argument, as FreeBSD 13's [`qsort_s`](https://github.com/freebsd/freebsd-src/blob/release/13.0.0/lib/libc/stdlib/qsort.c#L97-L127) (C11 Annex K) does. It returns `EINVAL` after calling the constraint handler when `n` or `es` exceeds `RSIZE_MAX`, or when `n` is not 0 and `a` or `cmp` is null; FreeBSD 11, which the rest of libc follows, has no `qsort_s`. An element size of 0 sorts nothing, where FreeBSD loops over the same address. The console's behaviour is unverified; the Quake homebrew (#6698) imports it from its `libOSMesa.so.8` From 791990ad3ec78e48faa5c5cc996fcd69cd9d1b02 Mon Sep 17 00:00:00 2001 From: orezraey Date: Thu, 22 Oct 2026 21:37:07 -0300 Subject: [PATCH 187/318] fix(libkernel): check the event flag name before the output as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp | 5 ++++- core/libs/tests/GuestKernelErrors.cpp | 7 +++++-- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index 4d2e1f7d1d..698b8ceb0e 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -25,6 +25,7 @@ static constexpr int SCE_KERNEL_ERROR_EACCES = 0x8002000D; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; +static constexpr int SCE_KERNEL_ERROR_EFAULT = 0x8002000E; static constexpr int SCE_KERNEL_ERROR_ENAMETOOLONG = 0x8002003F; static constexpr int SCE_KERNEL_ERROR_ECANCELED = 0x80020055; @@ -178,7 +179,8 @@ int APS5_VABI sceKernelCreateEventFlag(KernelEventFlag* ef, const char* name, ui } int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name) { - if (!ef || !name) return SCE_KERNEL_ERROR_EINVAL; + if (!name) return SCE_KERNEL_ERROR_EFAULT; + if (std::strlen(name) >= EVF_NAME_CAPACITY) return SCE_KERNEL_ERROR_ENAMETOOLONG; try { std::unique_lock lock(eventFlagRegistryMutex); std::shared_ptr target; @@ -190,6 +192,7 @@ int APS5_VABI sceKernelOpenEventFlag(KernelEventFlag* ef, const char* name) { break; } if (!target) return SCE_KERNEL_ERROR_ESRCH; + if (!ef) return SCE_KERNEL_ERROR_EFAULT; *ef = RegisterEventFlag(std::make_shared(EventFlagHandle{std::move(target), true})); } catch (const std::bad_alloc&) { return SCE_KERNEL_ERROR_ENOMEM; diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index a635a36c7f..8989624672 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -224,8 +224,10 @@ int main() { } KernelEventFlag opened = nullptr; - Require(sceKernelOpenEventFlag(nullptr, "test") == SCE_KERNEL_ERROR_EINVAL); - Require(sceKernelOpenEventFlag(&opened, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelOpenEventFlag(nullptr, "test") == SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelOpenEventFlag(&opened, nullptr) == SCE_KERNEL_ERROR_EFAULT && opened == nullptr); + Require(sceKernelOpenEventFlag(&opened, "") == SCE_KERNEL_ERROR_ESRCH && opened == nullptr); + Require(sceKernelOpenEventFlag(&opened, "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn") == SCE_KERNEL_ERROR_ENAMETOOLONG && opened == nullptr); Require(sceKernelOpenEventFlag(&opened, "SceShellCoreUtilAppFocus") == SCE_KERNEL_ERROR_ESRCH && opened == nullptr); Require(sceKernelCloseEventFlag(nullptr) == SCE_KERNEL_ERROR_ESRCH); @@ -242,6 +244,7 @@ int main() { KernelEventFlag sharedTwin = nullptr; Require(sceKernelCreateEventFlag(&sharedTwin, "test_open", EVF_ATTR_SHARED | EVF_ATTR_MULTI, 0, nullptr) == SCE_KERNEL_ERROR_EPERM && sharedTwin == nullptr); Require(sceKernelOpenEventFlag(&opened, "test_open") == SCE_OK && opened != nullptr && opened != ef && opened != plain); + Require(sceKernelOpenEventFlag(nullptr, "test_open") == SCE_KERNEL_ERROR_EFAULT); KernelEventFlag openedTwice = nullptr; Require(sceKernelOpenEventFlag(&openedTwice, "test_open") == SCE_OK && openedTwice != opened && openedTwice != ef); Require(sceKernelSetEventFlag(opened, 0x08) == SCE_OK); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 4fb32ed53b..dce2237b6e 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -59,7 +59,7 @@ - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console -- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, where the console faulted; with a name it cannot open, both return `SCE_KERNEL_ERROR_ESRCH`. - [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - when a call has both an invalid attribute and a name of 32 bytes or more, `SCE_KERNEL_ERROR_EINVAL` is returned, because the attribute checks run before the name length check. The console was only measured with long names and valid attributes (0x20 and 0x120), so which error it returns first is not known. Event flag handles are host addresses here, while the console returns small IDs. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked From d2927e0ec12d75ac5bfbc928f652effd39a8889a Mon Sep 17 00:00:00 2001 From: hypermist Date: Fri, 23 Oct 2026 14:19:46 +1300 Subject: [PATCH 188/318] fix(libc): follow libc.prx for strtof signs and hexadecimal round-up ERANGE Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SNqSvnFwRBz3D76n476yw6 --- core/libs/prx/libc/src/Strings.cpp | 9 ++++++--- core/libs/tests/GuestMath.cpp | 24 +++++++++++++++++++----- core/libs/tests/GuestWideStrings.cpp | 6 +++++- docs/dev/debt/libc.md | 2 +- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index cb3ab774ae..5d88f8c55e 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -153,7 +153,7 @@ TFloat NanValue(const NanSubject& subject, const char*& end) { return std::bit_cast((subject.negative ? 0x8000000000000000ull : 0) | 0x7ff8000000000000ull | payload); } else if constexpr (std::is_same_v) { end = NanSequenceEnd(subject.tail); - return std::bit_cast(0x7fc00000u); + return std::bit_cast((subject.negative ? 0x80000000u : 0) | 0x7fc00000u); } else { end = NanSequenceEnd(subject.tail); TFloat value{}; @@ -190,8 +190,10 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha const char* subject = text; while (std::isspace(static_cast(*subject))) ++subject; if (parsedEnd == text) { + if (subject[0] != '-') return TFloat{0}; + if constexpr (std::is_same_v) return subject[1] == '\0' ? TFloat{0} : -TFloat{0}; const bool special = std::strchr("iInN", subject[1]) != nullptr && subject[1] != '\0'; - return !std::is_same_v && subject[0] == '-' && !special ? -TFloat{0} : TFloat{0}; + return std::is_same_v && !special ? -TFloat{0} : TFloat{0}; } if (*subject == '+' || *subject == '-') ++subject; const bool hexadecimal = subject[0] == '0' && (subject[1] == 'x' || subject[1] == 'X') && parsedEnd > subject + 2; @@ -199,7 +201,8 @@ TFloat ParseFloat(const char* text, char** end, TFloat (*parse)(const char*, cha const float magnitude = value < 0 ? -value : value; if (value != 0 && magnitude < std::numeric_limits::min()) { const long double exact = std::strtold(text, nullptr); - errno = !hexadecimal && static_cast(value) != exact ? ERANGE : saved; + const bool roundedUp = magnitude == std::numeric_limits::denorm_min() && (exact < 0 ? -exact : exact) < magnitude; + errno = hexadecimal ? (roundedUp ? ERANGE : saved) : (static_cast(value) != exact ? ERANGE : saved); } return value; } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index c66c945324..86b42f45d2 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -437,8 +437,9 @@ static void CheckNanConversions() { std::fprintf(stderr, "atof(\"%s\")\n", test.text); correct = false; } + const std::uint32_t expectedSingle = (test.bits >> 63 != 0 ? 0x80000000u : 0) | 0x7fc00000u; const auto single = std::bit_cast(strtof_nid_postfix(test.text, &end)); - if (single != 0x7fc00000u || end != test.text + test.sequenceUsed) { + if (single != expectedSingle || end != test.text + test.sequenceUsed) { std::fprintf(stderr, "strtof(\"%s\") = 0x%08x\n", test.text, static_cast(single)); correct = false; } @@ -461,7 +462,7 @@ static void CheckNanConversions() { } if (test.wideOtherMeasured) { const auto wideSingle = std::bit_cast(wcstof_nid_postfix(wide, &wideEnd)); - const bool singleOk = wideSingle == 0x7fc00000u && wideEnd == wide + test.sequenceUsed; + const bool singleOk = wideSingle == expectedSingle && wideEnd == wide + test.sequenceUsed; const long double wideExtended = wcstold_nid_postfix(wide, &wideEnd); if (!singleOk || !ExtendedBits(wideExtended, 0xc000000000000000ull, 0x7fff) || wideEnd != wide + test.sequenceUsed) { std::fprintf(stderr, "wcstof/wcstold(\"%s\")\n", test.text); @@ -509,6 +510,12 @@ static void CheckFloatUnderflowErrno() { Require(strtof_nid_postfix("1.40129846432481707092372958328991613128026194187651577175706828388979108268586060148663818836212158203125e-45", &end) == 0x1p-149f && strtof_nid_postfix("1.17549435e-38", &end) == 0x1p-126f && errno == 7); Require(strtof_nid_postfix("0x1p-150", &end) == 0.0f && errno == ERANGE); errno = 7; + Require(strtof_nid_postfix("0x1.8p-150", &end) == 0x1p-149f && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("-0x0.0000018p-126", &end) == -0x1p-149f && errno == ERANGE); + errno = 7; + Require(strtof_nid_postfix("0x1.4p-149", &end) == 0x1p-149f && strtof_nid_postfix("0x1.cp-149", &end) == 0x1p-148f && errno == 7); + errno = 7; Require(strtof_nid_postfix("0x1p+200", &end) == HUGE_VALF && errno == ERANGE); errno = 7; Require(strtof_nid_postfix("1e-40", &end) == 1e-40f && errno == ERANGE); @@ -1259,13 +1266,14 @@ static void CheckLongDoubleRounding(long double value, std::int64_t expected, bo } static void CheckNoConversionZero() { - for (const char* text : {"", " ", "-", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { + for (const char* text : {"", " ", "-", " -", "+", "-.", "+.", "-x", "-.e1", "-e5", "- 1", " -."}) { char* end = nullptr; const double parsed = strtod_nid_postfix(text, &end); Require(parsed == 0. && !std::signbit(parsed) && end == text); const bool negative = std::strchr(text, '-') != nullptr; + const bool loneSign = negative && text[std::strlen(text) - 1] == '-'; const float parsedFloat = strtof_nid_postfix(text, &end); - Require(parsedFloat == 0.f && std::signbit(parsedFloat) == negative && end == text); + Require(parsedFloat == 0.f && std::signbit(parsedFloat) == (negative && !loneSign) && end == text); const long double parsedLong = strtold_nid_postfix(text, &end); Require(parsedLong == 0.L && std::signbit(parsedLong) == negative && end == text); Require(!std::signbit(atof_nid_postfix(text))); @@ -1273,9 +1281,15 @@ static void CheckNoConversionZero() { for (const char* text : {"-i", "-in", "-N", "-nax"}) { char* end = nullptr; const float parsedFloat = strtof_nid_postfix(text, &end); - Require(parsedFloat == 0.f && !std::signbit(parsedFloat) && end == text); + Require(parsedFloat == 0.f && std::signbit(parsedFloat) && end == text); Require(!std::signbit(strtold_nid_postfix(text, nullptr))); } + for (const char* text : {"-nan", "-NAN", " -nan(5)"}) { + char* end = nullptr; + const float parsedFloat = strtof_nid_postfix(text, &end); + Require(std::bit_cast(parsedFloat) == 0xffc00000u && end == text + std::strlen(text)); + } + Require(std::bit_cast(strtof_nid_postfix("nan", nullptr)) == 0x7fc00000u); char* end = nullptr; const char negativeZero[] = "-0.0x"; const double parsed = strtod_nid_postfix(negativeZero, &end); diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 268d223c37..36e299c22f 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -365,11 +365,15 @@ int main() { require(wcstof_nid_postfix(u"1e-45", nullptr) == 0x1p-149f && *__error_nid_postfix() == ERANGE); *__error_nid_postfix() = 0; require(wcstof_nid_postfix(u"0x1p-149", nullptr) == 0x1p-149f && *__error_nid_postfix() == 0); + require(wcstof_nid_postfix(u"0x1.8p-150", nullptr) == 0x1p-149f && *__error_nid_postfix() == ERANGE); + const char16_t* loneSign = u" -"; + const float loneSignValue = wcstof_nid_postfix(loneSign, &end); + require(loneSignValue == 0.f && !std::signbit(loneSignValue) && end == loneSign && std::signbit(wcstold_nid_postfix(loneSign, &end))); const char16_t* half = u"-1.5é"; require(wcstold_nid_postfix(half, &end) == -1.5L && end == half + 4); const char16_t* wideNan = u" -nan(0x123)é"; require(std::bit_cast(wcstod_nid_postfix(wideNan, &end)) == 0xfff8000000000123u && end == wideNan + 12); - require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0x7fc00000u && end == wideNan + 12); + require(std::bit_cast(wcstof_nid_postfix(wideNan, &end)) == 0xffc00000u && end == wideNan + 12); const long double wideExtended = wcstold_nid_postfix(wideNan, &end); std::uint64_t significand; std::memcpy(&significand, &wideExtended, sizeof(significand)); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e9cd7991bc..9471b1b5a8 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -62,7 +62,7 @@ - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced -- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `ERANGE` follows the `libc.prx` bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509), both run side by side with this libc on a Linux host: a decimal input whose result is a nonzero subnormal sets it only when inexact, a hexadecimal one never does, and underflow to zero and overflow set it. Both builds also read a `NAN(...)` sequence in `strtof` as their `strtod` does, keep the sign and take a payload from its leading digits (`nan(0x123)` is `0x7fc00123`, `-nan(5)` is `0xffc00005`); that is not reproduced. Decimal `strtod` sets `ERANGE` for a nonzero subnormal in J03547016 but not in J03848509 or `libSceLibcInternal`, whose result is kept. +- [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `ERANGE` follows the `libc.prx` bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509), both run side by side with this libc on a Linux host: a decimal input whose result is a nonzero subnormal sets it only when inexact, a hexadecimal one only when it lies below the smallest subnormal and rounds up to it, and underflow to zero and overflow set it. Both builds also take a `NAN(...)` payload in `strtof` from the sequence's leading digits (`nan(0x123)` is `0x7fc00123`, `-nan(5)` is `0xffc00005`); the sign is kept here, the payload is not. `libSceLibcInternal`'s `_Stof` differs: no `ERANGE` for hexadecimal input, a positive `nan`, and after a minus sign a failed parse is `-0.0` for a lone sign and `+0.0` before an `inf`/`nan` spelling, the reverse of `libc.prx`. Decimal `strtod` sets `ERANGE` for a nonzero subnormal in J03547016 but not in J03848509 or `libSceLibcInternal`, whose result is kept. - [Guest time conversions](../../../core/libs/prx/libc/src/Time.cpp) rewrite the host process `TZ` when a guest changes its own `TZ`, so other host consumers of that variable (including libSceRtc local time) shift too; code outside libc does not take the zone lock. Zone syntax follows the host C runtime, not FreeBSD: Windows only accepts the `std offset [dst]` form, treats an empty guest `TZ` as `UTC0` and reads `:Europe/Berlin` as an abbreviation (for example `rop`); only Linux honours zone file names. If the guest accepts a `TZ` value that the host rejects (on Windows, for example, one longer than the host environment limit), conversions throw `std::runtime_error`. A `tm_zone` saved before a `TZ` change can refer to storage now holding the new zone name, changing `%Z` output. - [__isfinitel](../../../core/libs/prx/libc/src/Math.cpp) - classifies the x87 80-bit exponent without narrowing to double or performing floating-point comparisons, following [FreeBSD msun](https://github.com/freebsd/freebsd-src/blob/main/lib/msun/src/s_isfinite.c). The signature and classification follow FreeBSD; console behaviour is unmeasured. Requires little-endian x87 long double stored in 16 bytes, as the existing guest long-double ABI does. From 71932c0a56447615c74c5edca1e1272c5ec8e870 Mon Sep 17 00:00:00 2001 From: sivasdeadknight Date: Thu, 22 Oct 2026 20:55:01 +0200 Subject: [PATCH 189/318] fix(libSceNpManager): validate request ids for abort, delete and poll sceNpAbortRequest and sceNpDeleteRequest return SCE_NP_ERROR_REQUEST_NOT_FOUND for an id that was never created or was deleted; sceNpPollAsync does too and additionally rejects a null result pointer with SCE_NP_ERROR_INVALID_ARGUMENT before reading the id, without writing the caller's result. A live request keeps its signed-out completion. This pins the validation half of shadPS4's np_manager model that issue #9277 reports; the kind, completion state and WaitAsync publication half stays open, documented in the debt entry, and the sceNpWaitAsync export itself stays with PR #9282. Tests: guest_np_manager tests the four absent/deleted probes (0, -1, 0x7fffffff, a deleted id) for all three calls, output preservation, null-result precedence and the live-id paths; guest_np_reachability unchanged. Both pass on Linux; the new asserts failed on unpatched exports first. --- core/libs/prx/libSceNpManager/Export.cpp | 11 +++++---- core/libs/tests/GuestNpManager.cpp | 30 ++++++++++++++++++++++++ docs/dev/debt/libSceNpManager.md | 5 ++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 15fd862b97..c50ab5b01f 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -673,8 +673,8 @@ int APS5_VABI sceNpManagerIntLoginGetAccessToken(void) { } int APS5_VABI sceNpAbortRequest(int req_id) { - (void)req_id; - return 0; + std::lock_guard lock(requestMutex); + return liveRequests.count(req_id) ? 0 : SCE_NP_ERROR_REQUEST_NOT_FOUND; } int APS5_VABI sceNpCheckCallback(void) { @@ -724,6 +724,7 @@ int APS5_VABI sceNpCreateRequest(void) { int APS5_VABI sceNpDeleteRequest(int req_id) { std::lock_guard lock(requestMutex); + if (!liveRequests.count(req_id)) return SCE_NP_ERROR_REQUEST_NOT_FOUND; liveRequests.erase(req_id); return 0; } @@ -834,8 +835,10 @@ int APS5_VABI sceNpHasSignedUp(int user_id, bool* has_signed_up) { } int APS5_VABI sceNpPollAsync(int req_id, int* result) { - (void)req_id; - if (result) *result = SCE_NP_ERROR_SIGNED_OUT; + if (!result) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(requestMutex); + if (!liveRequests.count(req_id)) return SCE_NP_ERROR_REQUEST_NOT_FOUND; + *result = SCE_NP_ERROR_SIGNED_OUT; return NP_POLL_ASYNC_FINISHED; } diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index 67e63ff3b0..ef285cab3a 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -33,6 +33,8 @@ extern "C" { int APS5_VABI sceNpCreateRequest(void); int APS5_VABI sceNpCreateAsyncRequest(const NpCreateAsyncRequestParameter* param); int APS5_VABI sceNpDeleteRequest(int reqId); +int APS5_VABI sceNpAbortRequest(int reqId); +int APS5_VABI sceNpPollAsync(int reqId, int* result); int APS5_VABI sceNpSetTimeout(int reqId, std::int32_t resolveRetry, std::uint32_t resolveTimeout, std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); } @@ -238,6 +240,33 @@ void CheckRequestTimeouts() { Require(sceNpSetTimeout(asyncId, 2, 0, 0, 0, 0) == RequestNotFound, "deleted async request"); } +void CheckAsyncRequestLifecycle() { + NpCreateAsyncRequestParameter param{}; + const int unusedAsyncResult = sceNpCreateAsyncRequest(¶m); + Require(unusedAsyncResult > 0, "sceNpCreateAsyncRequest must return a positive request id"); + const int deletedId = sceNpCreateAsyncRequest(¶m); + Require(sceNpDeleteRequest(deletedId) == 0, "initial delete"); + + constexpr int resultSentinel = 0x5a5a5a5a; + const int unknownIds[] = {0, -1, 0x7fffffff, deletedId}; + for (const int id : unknownIds) { + int result = resultSentinel; + Require(sceNpPollAsync(id, &result) == RequestNotFound, "poll must reject an absent or deleted id"); + Require(result == resultSentinel, "poll must not touch the result for an absent or deleted id"); + Require(sceNpPollAsync(id, nullptr) == InvalidArgument, + "poll must reject a null result before the id"); + Require(sceNpAbortRequest(id) == RequestNotFound, "abort must reject an absent or deleted id"); + Require(sceNpDeleteRequest(id) == RequestNotFound, "delete must reject an absent or deleted id"); + } + + int result = resultSentinel; + Require(sceNpPollAsync(unusedAsyncResult, &result) == 0, "poll on a live async request"); + Require(result == SignedOut, "poll must report the signed-out result on a live async request"); + Require(sceNpAbortRequest(unusedAsyncResult) == 0, "abort on a live request"); + Require(sceNpDeleteRequest(unusedAsyncResult) == 0, "delete after abort"); + Require(sceNpDeleteRequest(unusedAsyncResult) == RequestNotFound, "second delete"); +} + void CheckOnlineIdQueries() { NpOnlineId onlineId{}; std::memcpy(onlineId.data, "player", 6); @@ -394,6 +423,7 @@ int main() { CheckStateCallbacksA(); CheckLegacyStateCallback(); CheckRequestTimeouts(); + CheckAsyncRequestLifecycle(); CheckPlusAndContentRestriction(); NpId npId{}; std::memset(&npId, 0x5a, sizeof(npId)); diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index a484f93b3a..0595fc92c4 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -29,3 +29,8 @@ - [sceNpIsPlusMember and sceNpSetContentRestriction](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the signatures and checks follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp): a null output or restriction is `INVALID_ARGUMENT`, a restriction size other than 0x18 is `INVALID_SIZE` (0x80550011), and a negative default age, more than 256 age restrictions or a count without an array is `INVALID_ARGUMENT`. Any user is reported as not a Plus member, and a valid restriction is accepted and not stored. Not measured on a PS5 - [sceNpCheckCallbackForLib](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - not measured on a PS5: it delivers the pending state callbacks like `sceNpCheckCallback`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_manager.cpp) does with its pending state events; shadPS4's `sceNpCheckCallback` also runs other registered NP callbacks, which this library does not keep + +- [sceNpAbortRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - a request id that was never created or was deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` and a live request returns 0, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp) does in `sceNpAbortRequest`; shadPS4 there also moves the request into an `Aborted` state that its later requests read as `ABORTED`, and whether aborting on the console changes a live request's completion is not measured on a PS5 ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)), so nothing in this library reads an abort state and the id stays live +- [sceNpDeleteRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - a request id that was never created or was deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND`, and a live request is removed and returns 0, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp) does in `sceNpDeleteRequest` ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)) +- [sceNpPollAsync](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - a null result pointer returns `SCE_NP_ERROR_INVALID_ARGUMENT` before the id is read, a request id that was never created or was deleted returns `SCE_NP_ERROR_REQUEST_NOT_FOUND` without writing the caller's result, and a live request keeps its signed-out completion `0` with `SCE_NP_ERROR_SIGNED_OUT`, as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp) does in `sceNpPollAsync` ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)). The parts of that model beyond the validation are not implemented: shadPS4 keeps a per-request state machine decorated by `sceNpCheckNpAvailability` and neighbors and refuses `InvalidId` for a ready async request in wait/poll, whose completion publication, poll-versus-wait precedence and already-complete errors remain unattributable to the PS5 +- [sceNpCreateAsyncRequest](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the parameter is not read ([NpCreateAsyncRequestParameter](../../../core/libs/SceTypes.hpp) is a 0x40-byte opaque blob, so shadPS4's nonnull check and `size == sizeof` reject ([81be949167d0060b08f26dc3e055a0b640d129c8](https://github.com/shadps4-emu/shadPS4/blob/81be949167d0060b08f26dc3e055a0b640d129c8/src/core/libraries/np/np_manager.cpp)) cannot be applied without inventing a layout) and no async kind or completion state is stored until the console's contract for them is evidenced ([#9277](https://github.com/boykopovar/AnyPS5/issues/9277)); `sceNpWaitAsync` (`jyi5p9XWUSs`) has no export here yet and stays with [PR #9282](https://github.com/boykopovar/AnyPS5/pull/9282) From 7d8bbff86da8faf88b572fae553f39eb05ffc6e5 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:13:47 +0800 Subject: [PATCH 190/318] feat(libScePad): implement Lotus IR key toggles and virtual-device settings scePadSetLotusEnableIrKey and scePadSetLotusDisableIrKey validate the pad handle, returning PAD_ERROR_INVALID_HANDLE (0x80920002) for any invalid handle and 0 on open pads as silent stubs. scePadVirtualDeviceDisableButtonRemapping acts as a silent stub returning 0. scePadVirtualDeviceGetRemoteSetting recovers the 64-byte remote setting layout, rejecting null buffers and non-positive device IDs with PAD_ERROR_INVALID_ARG (0x80920001) and returning PAD_ERROR_DEVICE_NOT_CONNECTED (0x80920007). --- core/libs/prx/libScePad/Export.cpp | 20 +++++++++++--------- core/libs/tests/GuestPad.cpp | 15 +++++++++++++++ docs/dev/debt/libScePad.md | 3 +++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index 057d434611..dd6c2394f3 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -22,7 +22,10 @@ struct PadUsbDetectionState; struct PadVrUsbConnectionStatus; struct PadDeviceInfo; struct PadLicenseControllerInformation; -struct PadRemoteSetting; +struct PadRemoteSetting { + std::uint8_t data[64]; +}; +static_assert(sizeof(PadRemoteSetting) == 64); struct PadVirtualTriggerSetting; struct PadProcessFocusParameter; struct PadDataExtended; @@ -663,13 +666,13 @@ int APS5_VABI scePadGetLotusIrCodeSetResult(std::int32_t handle, std::int32_t* r } int APS5_VABI scePadSetLotusEnableIrKey(std::int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + return PAD_OK; } int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t handle) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; + return PAD_OK; } int APS5_VABI scePadGetUsbDetectionState(std::int32_t deviceId, PadUsbDetectionState* state) { @@ -1018,8 +1021,8 @@ int APS5_VABI scePadVirtualDeviceInsertTriggerInternalState(std::int32_t deviceI } int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t deviceId, PadRemoteSetting* setting) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (setting == nullptr || deviceId <= 0) return PAD_ERROR_INVALID_ARG; + return PAD_ERROR_DEVICE_NOT_CONNECTED; } int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(std::int32_t handle, const PadVirtualTriggerSetting* setting) { @@ -1028,8 +1031,7 @@ int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(std::int32_t handle, co } int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(bool disabled) { - NotImplemented_nid_no_patch(__func__); - return 0; + return PAD_OK; } int APS5_VABI scePadSetShareButtonMaskForRemotePlay(std::uint8_t mask) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 6dfa8c9a9c..2783ffce72 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -38,6 +38,10 @@ int APS5_VABI scePadGetExtControllerInformation(int, void*); int APS5_VABI scePadResetLightBar(int); int APS5_VABI scePadSetLightBar(int, const PadLightBarParam*); int APS5_VABI scePadSetMotionSensorState(int, bool); +int APS5_VABI scePadSetLotusEnableIrKey(std::int32_t); +int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t); +int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(bool); +int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t, void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -502,4 +506,15 @@ int main() { Require(scePadSetAngularVelocityDeadbandState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetAngularVelocityBiasCorrectionState(handle, false) == PAD_OK); Require(scePadSetAngularVelocityBiasCorrectionState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetLotusEnableIrKey(handle) == PAD_OK); + Require(scePadSetLotusEnableIrKey(handle + 1) == PAD_ERROR_INVALID_HANDLE); + Require(scePadSetLotusDisableIrKey(handle) == PAD_OK); + Require(scePadSetLotusDisableIrKey(handle + 1) == PAD_ERROR_INVALID_HANDLE); + Require(scePadVirtualDeviceDisableButtonRemapping(true) == PAD_OK); + Require(scePadVirtualDeviceDisableButtonRemapping(false) == PAD_OK); + std::array remoteSetting{}; + Require(scePadVirtualDeviceGetRemoteSetting(1, remoteSetting.data()) == static_cast(0x80920007)); + Require(scePadVirtualDeviceGetRemoteSetting(1, nullptr) == PAD_ERROR_INVALID_ARG); + Require(scePadVirtualDeviceGetRemoteSetting(0, remoteSetting.data()) == PAD_ERROR_INVALID_ARG); + Require(scePadVirtualDeviceGetRemoteSetting(-1, remoteSetting.data()) == PAD_ERROR_INVALID_ARG); } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index c8fa44cf9d..e740790bfe 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -3,6 +3,8 @@ ### Silent stubs - [scePadEnableExtensionPort](../../../core/libs/prx/libScePad/Export.cpp) returns success without changing extension-port state. The boolean argument is recovered; connected-device behavior is not modeled. +- [scePadSetLotusEnableIrKey and scePadSetLotusDisableIrKey](../../../core/libs/prx/libScePad/Export.cpp) return success on valid open handle without hardware Lotus IR key dispatching. +- [scePadVirtualDeviceDisableButtonRemapping](../../../core/libs/prx/libScePad/Export.cpp) returns success without changing virtual-device remapping tables. ### Unknown function info @@ -20,6 +22,7 @@ - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown +- [scePadVirtualDeviceGetRemoteSetting](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - checks the output pointer and non-zero device id, returning `PAD_ERROR_DEVICE_NOT_CONNECTED` (0x80920007) when no virtual device is connected. Remote setting payload uses a 64-byte record whose internal field meanings remain unverified on hardware. - [libScePad internal data and device control](../../../core/libs/prx/libScePad/Export.cpp) - kernel-owned version, telemetry, Lotus-button and update payload fields remain unresolved. Lotus-button parameter units and the three blinking byte meanings are unknown. Port information occupies 152 bytes; history records occupy 72 bytes; Leonardo unique data occupies 24 bytes. Their complete field semantics remain unresolved. From 12a14ac1a517863335603d6d5762cec1ff1ac4f1 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Fri, 23 Oct 2026 04:03:49 +0100 Subject: [PATCH 191/318] feat(libSceAgcVsh): declare 150 pending exports --- core/libs/prx/libSceAgcVsh/Export.cpp | 900 +++++++++++++++++++++++++- docs/dev/debt/libSceAgcVsh.md | 5 + 2 files changed, 904 insertions(+), 1 deletion(-) create mode 100644 docs/dev/debt/libSceAgcVsh.md diff --git a/core/libs/prx/libSceAgcVsh/Export.cpp b/core/libs/prx/libSceAgcVsh/Export.cpp index fb34206c21..0b65c5fa98 100644 --- a/core/libs/prx/libSceAgcVsh/Export.cpp +++ b/core/libs/prx/libSceAgcVsh/Export.cpp @@ -5,6 +5,904 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAgcDcbCopyData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("1rZSWUv1IRc", sceAgcDcbCopyData_nid_no_patch); + +int APS5_VABI sceAgcBranchPatchSetElseTarget_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("QmfvaYpsOcI", sceAgcBranchPatchSetElseTarget_nid_no_patch); + +int APS5_VABI sceAgcCbSetShRegistersDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("UZbQjYAwwXM", sceAgcCbSetShRegistersDirect_nid_no_patch); + +int APS5_VABI sceAgcCreateShader_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("f3dg2CSgRKY", sceAgcCreateShader_nid_no_patch); + +int APS5_VABI sceAgcDcbSetNumInstancesGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("6DFuRKT4C9w", sceAgcDcbSetNumInstancesGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetUcRegisterDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("aP1Ki9G3++4", sceAgcDcbSetUcRegisterDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexIndirectMultiGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("r98I08t+LOg", sceAgcDcbDrawIndexIndirectMultiGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbAtomicGdsGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("1tB0xkLNjcw", sceAgcDcbAtomicGdsGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbCopyDataGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("CbQh3DKMSno", sceAgcAcbCopyDataGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbWaitUntilSafeForRendering_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("GPbUp9jXQa8", sceAgcAcbWaitUntilSafeForRendering_nid_no_patch); + +int APS5_VABI sceAgcAsyncCondExecPatchSetEnd_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("k-JpyR2dYAM", sceAgcAsyncCondExecPatchSetEnd_nid_no_patch); + +int APS5_VABI sceAgcDcbDmaDataGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("2ccJz9LQI+w", sceAgcDcbDmaDataGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetUcRegistersIndirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("hvUfkUIQcOE", sceAgcDcbSetUcRegistersIndirect_nid_no_patch); + +int APS5_VABI sceAgcGetRegisterDefaults2_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("2JtWUUiYBXs", sceAgcGetRegisterDefaults2_nid_no_patch); + +int APS5_VABI sceAgcCbReleaseMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("wr23dPKyWc0", sceAgcCbReleaseMem_nid_no_patch); + +int APS5_VABI sceAgcCondExecPatchSetEnd_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ORWsxIbk4TE", sceAgcCondExecPatchSetEnd_nid_no_patch); + +int APS5_VABI sceAgcDcbAtomicGds_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("pH3-dfRpfA0", sceAgcDcbAtomicGds_nid_no_patch); + +int APS5_VABI sceAgcDcbSetCxRegistersIndirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("GBCh3zCihoU", sceAgcDcbSetCxRegistersIndirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbSetUcRegisterRangeDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("MDLD5Ly94Xk", sceAgcCbSetUcRegisterRangeDirect_nid_no_patch); + +int APS5_VABI sceAgcDcbEventWrite_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("aJf+j5yntiU", sceAgcDcbEventWrite_nid_no_patch); + +int APS5_VABI sceAgcDcbWriteData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("i1jyy49AjXU", sceAgcDcbWriteData_nid_no_patch); + +int APS5_VABI sceAgcSetUcRegIndirectPatchSetAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("6lNcCp+fxi4", sceAgcSetUcRegIndirectPatchSetAddress_nid_no_patch); + +int APS5_VABI sceAgcCbCondWriteGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("FuVbkyKlf+s", sceAgcCbCondWriteGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbQueueEndOfShaderActionGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("zg6u-N6Otxs", sceAgcDcbQueueEndOfShaderActionGetSize_nid_no_patch); + +int APS5_VABI sceAgcGetPacketSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Lkf86B98qPc", sceAgcGetPacketSize_nid_no_patch); + +int APS5_VABI sceAgcAcbAcquireMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("KT-hTp-Ch14", sceAgcAcbAcquireMem_nid_no_patch); + +int APS5_VABI sceAgcAcbEventWriteGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Y-5vneiBtzk", sceAgcAcbEventWriteGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetIndexIndirectArgs_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("0o3VDdtA6nM", sceAgcDcbSetIndexIndirectArgs_nid_no_patch); + +int APS5_VABI sceAgcAcbDmaData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("-RnpfpxIhec", sceAgcAcbDmaData_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexIndirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("t1vNu082-jM", sceAgcDcbDrawIndexIndirect_nid_no_patch); + +int APS5_VABI sceAgcDcbPrimeUtcl2GetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("KjPeVduz6jU", sceAgcDcbPrimeUtcl2GetSize_nid_no_patch); + +int APS5_VABI sceAgcAsyncCondExecPatchSetCommandAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("3ZWa3AoyWZQ", sceAgcAsyncCondExecPatchSetCommandAddress_nid_no_patch); + +int APS5_VABI sceAgcAcbAcquireMemGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ewobAQeMo5k", sceAgcAcbAcquireMemGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("AFIh8SQkYlQ", sceAgcDcbSetIndexIndirectArgsGetSize_nid_no_patch); + +int APS5_VABI sceAgcCreateInterpolantMapping_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("pdEV7bI6COI", sceAgcCreateInterpolantMapping_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexIndirectMulti_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ypVBz4uPKcQ", sceAgcDcbDrawIndexIndirectMulti_nid_no_patch); + +int APS5_VABI sceAgcDcbEventWriteGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("C4l9fB17t8w", sceAgcDcbEventWriteGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetUcRegisterDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("w4-d0n60hdo", sceAgcDcbSetUcRegisterDirect_nid_no_patch); + +int APS5_VABI sceAgcQueueEndOfPipeActionPatchAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("0fWWK5uG9rQ", sceAgcQueueEndOfPipeActionPatchAddress_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndirectMultiGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("pYoKs3lPy88", sceAgcDcbDrawIndirectMultiGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetBaseDrawIndirectArgsGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("MMlmJAL7N5w", sceAgcDcbSetBaseDrawIndirectArgsGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetNumInstances_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("tSBxhAPyytQ", sceAgcDcbSetNumInstances_nid_no_patch); + +int APS5_VABI sceAgcDcbWaitOnAddressGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("43WJ08sSugE", sceAgcDcbWaitOnAddressGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbAtomicGds_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("cduV1f0dcGQ", sceAgcAcbAtomicGds_nid_no_patch); + +int APS5_VABI sceAgcDcbSetBoolPredicationEnableGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("yheJGN-ay+A", sceAgcDcbSetBoolPredicationEnableGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbAtomicMemGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("da1Sm8-QDoU", sceAgcAcbAtomicMemGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetCfRegisterRangeDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("BVFg3CWU6Eo", sceAgcDcbSetCfRegisterRangeDirect_nid_no_patch); + +int APS5_VABI sceAgcDcbCondExecGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ou16V5hh5sg", sceAgcDcbCondExecGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexMultiInstanced_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Rlx+bykm0r0", sceAgcDcbDrawIndexMultiInstanced_nid_no_patch); + +int APS5_VABI sceAgcDcbSetCxRegistersIndirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ZvwO9euwYzc", sceAgcDcbSetCxRegistersIndirect_nid_no_patch); + +int APS5_VABI sceAgcCbSetShRegistersDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("yUBESvCCJ4I", sceAgcCbSetShRegistersDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcCondExecPatchSetCommandAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("YWTKOju587o", sceAgcCondExecPatchSetCommandAddress_nid_no_patch); + +int APS5_VABI sceAgcRewindPatchSetRewindState_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ziVA3whp3p4", sceAgcRewindPatchSetRewindState_nid_no_patch); + +int APS5_VABI sceAgcQueueEndOfPipeActionPatchGcrCntl_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("J8YCgfKAMQs", sceAgcQueueEndOfPipeActionPatchGcrCntl_nid_no_patch); + +int APS5_VABI sceAgcWaitRegMemPatchReference_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("7nOoijNPvEU", sceAgcWaitRegMemPatchReference_nid_no_patch); + +int APS5_VABI sceAgcAcbRewind_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("DwICrVxerkY", sceAgcAcbRewind_nid_no_patch); + +int APS5_VABI sceAgcAcbCondExecGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ozKzBP4aki4", sceAgcAcbCondExecGetSize_nid_no_patch); + +int APS5_VABI sceAgcSuspendPointAndCheckStatus_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("b+fis+WZ3Ig", sceAgcSuspendPointAndCheckStatus_nid_no_patch); + +int APS5_VABI sceAgcAcbWaitRegMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("htn36gPnBk4", sceAgcAcbWaitRegMem_nid_no_patch); + +int APS5_VABI sceAgcDcbAcquireMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("57labkp+rSQ", sceAgcDcbAcquireMem_nid_no_patch); + +int APS5_VABI sceAgcSetCxRegIndirectPatchSetNumRegisters_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("whb1RL7K4Ss", sceAgcSetCxRegIndirectPatchSetNumRegisters_nid_no_patch); + +int APS5_VABI sceAgcDcbContextStateOp_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("HabmgqPwPw0", sceAgcDcbContextStateOp_nid_no_patch); + +int APS5_VABI sceAgcAcbWriteData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("eZ4+17OQz4Q", sceAgcAcbWriteData_nid_no_patch); + +int APS5_VABI sceAgcAcbDmaDataGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("M0ttm8h7SKA", sceAgcAcbDmaDataGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbSetShRegisterRangeDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("bxGoVxpdSPQ", sceAgcCbSetShRegisterRangeDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbNop_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("LtTouSCZjHM", sceAgcCbNop_nid_no_patch); + +int APS5_VABI sceAgcDcbResetQueue_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("TRO721eVt4g", sceAgcDcbResetQueue_nid_no_patch); + +int APS5_VABI sceAgcDcbDmaData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("WmAc2MEj6Io", sceAgcDcbDmaData_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("6ee9Hd3EWXQ", sceAgcDcbDrawIndexGetSize_nid_no_patch); + +int APS5_VABI sceAgcDmaDataPatchSetDstAddressOrOffset_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("IxYiarKlXxM", sceAgcDmaDataPatchSetDstAddressOrOffset_nid_no_patch); + +int APS5_VABI sceAgcCbQueueEndOfPipeActionGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("hL7C0IRpWZI", sceAgcCbQueueEndOfPipeActionGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetFlip_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("YUeqkyT7mEQ", sceAgcDcbSetFlip_nid_no_patch); + +int APS5_VABI sceAgcDcbSetMarker_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("QhCbS4X9Rl8", sceAgcDcbSetMarker_nid_no_patch); + +int APS5_VABI sceAgcDcbSetPredication_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("bbFueFP+J4k", sceAgcDcbSetPredication_nid_no_patch); + +int APS5_VABI sceAgcDcbAtomicMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("1-gUn1PI4Sw", sceAgcDcbAtomicMem_nid_no_patch); + +int APS5_VABI sceAgcGetRegisterDefaults2Internal_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("wRbq6ZjNop4", sceAgcGetRegisterDefaults2Internal_nid_no_patch); + +int APS5_VABI sceAgcSetShRegIndirectPatchAddRegisters_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("z2duB-hHQSM", sceAgcSetShRegIndirectPatchAddRegisters_nid_no_patch); + +int APS5_VABI sceAgcDcbPrimeUtcl2_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("jt3pl7EN17o", sceAgcDcbPrimeUtcl2_nid_no_patch); + +int APS5_VABI sceAgcAcbRewindGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("0ZOG0jc9nRg", sceAgcAcbRewindGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetZPassPredicationEnableGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("XN+Iuu7XsM8", sceAgcDcbSetZPassPredicationEnableGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbWaitUntilSafeForRendering_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("MWiElSNE8j8", sceAgcDcbWaitUntilSafeForRendering_nid_no_patch); + +int APS5_VABI sceAgcBranchPatchSetThenTarget_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("xb8VgcXQhvI", sceAgcBranchPatchSetThenTarget_nid_no_patch); + +int APS5_VABI sceAgcSetCxRegIndirectPatchSetAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("vcmNN+AAXnY", sceAgcSetCxRegIndirectPatchSetAddress_nid_no_patch); + +int APS5_VABI sceAgcDcbAtomicMemGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("oz6zQq1JwCE", sceAgcDcbAtomicMemGetSize_nid_no_patch); + +int APS5_VABI sceAgcUpdatePrimState_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Y3ymLfZ1384", sceAgcUpdatePrimState_nid_no_patch); + +int APS5_VABI sceAgcCbNopGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("t7PlZ9nt5Lc", sceAgcCbNopGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbGetLodStats_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("vuSXe69VILM", sceAgcDcbGetLodStats_nid_no_patch); + +int APS5_VABI sceAgcWaitRegMemPatchAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("3KDcnM3lrcU", sceAgcWaitRegMemPatchAddress_nid_no_patch); + +int APS5_VABI sceAgcQueueEndOfPipeActionPatchData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("MlEw1feXcjg", sceAgcQueueEndOfPipeActionPatchData_nid_no_patch); + +int APS5_VABI sceAgcGetRegisterDefaultsInternal_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("uIwxsqDlHRc", sceAgcGetRegisterDefaultsInternal_nid_no_patch); + +int APS5_VABI sceAgcInit_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("kW3GLb7QfPg", sceAgcInit_nid_no_patch); + +int APS5_VABI sceAgcDcbDispatchIndirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("w8HVkEeXPv8", sceAgcDcbDispatchIndirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbCondExec_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("BIPexNBSGog", sceAgcDcbCondExec_nid_no_patch); + +int APS5_VABI sceAgcAcbSetMarker_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("xAeBOa0A3kk", sceAgcAcbSetMarker_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexOffsetGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("qMlfB1ZhMDc", sceAgcDcbDrawIndexOffsetGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbJump_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("e1DFTg+Sd8U", sceAgcAcbJump_nid_no_patch); + +int APS5_VABI sceAgcDcbSetIndexCount_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("8N2tmT3jmC8", sceAgcDcbSetIndexCount_nid_no_patch); + +int APS5_VABI sceAgcDcbSetShRegistersIndirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("nNlUtdDDvZ0", sceAgcDcbSetShRegistersIndirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbStallCommandBufferParserGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("+u6dKSLWM2o", sceAgcDcbStallCommandBufferParserGetSize_nid_no_patch); + +int APS5_VABI sceAgcSuspendPoint_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("h9z6+0hEydk", sceAgcSuspendPoint_nid_no_patch); + +int APS5_VABI sceAgcWaitRegMemPatchMask_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("hXAnLgDHCoI", sceAgcWaitRegMemPatchMask_nid_no_patch); + +int APS5_VABI sceAgcAcbDispatchIndirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("PxKWV2fVAps", sceAgcAcbDispatchIndirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbAtomicGdsGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("hcIxS8pmXF4", sceAgcAcbAtomicGdsGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbCondWrite_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("7toV+elXqNM", sceAgcCbCondWrite_nid_no_patch); + +int APS5_VABI sceAgcDcbEndOcclusionQueryGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("P1CugZ99Uzc", sceAgcDcbEndOcclusionQueryGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetPredicationDisableGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("vLrBL8DQiz8", sceAgcDcbSetPredicationDisableGetSize_nid_no_patch); + +int APS5_VABI sceAgcLinkShaders_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("MqAdbRMdNz4", sceAgcLinkShaders_nid_no_patch); + +int APS5_VABI sceAgcDcbWaitRegMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("VmW0Tdpy420", sceAgcDcbWaitRegMem_nid_no_patch); + +int APS5_VABI sceAgcGetRegisterDefaults_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Wi82ArQtAwg", sceAgcGetRegisterDefaults_nid_no_patch); + +int APS5_VABI sceAgcCbSetShRegisterRangeDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("n2fD4A+pb+g", sceAgcCbSetShRegisterRangeDirect_nid_no_patch); + +int APS5_VABI sceAgcSetCxRegIndirectPatchAddRegisters_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("d-6uF9sZDIU", sceAgcSetCxRegIndirectPatchAddRegisters_nid_no_patch); + +int APS5_VABI sceAgcDcbDispatchIndirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("CtB+A9-VxO0", sceAgcDcbDispatchIndirect_nid_no_patch); + +int APS5_VABI sceAgcCbMemSemaphore_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("vHX9guneRBY", sceAgcCbMemSemaphore_nid_no_patch); + +int APS5_VABI sceAgcCbSetUcRegisterRangeDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("JOWmDrl+j20", sceAgcCbSetUcRegisterRangeDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbDispatch_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("k3GhuSNmBLU", sceAgcCbDispatch_nid_no_patch); + +int APS5_VABI sceAgcDcbCopyDataGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("b5u0Jzm8TF8", sceAgcDcbCopyDataGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexMultiInstancedGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("mR9j7+SfM34", sceAgcDcbDrawIndexMultiInstancedGetSize_nid_no_patch); + +int APS5_VABI sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("cdDRpqcFGbU", sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate_nid_no_patch); + +int APS5_VABI sceAgcSetShRegIndirectPatchSetAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Qrj4c+61z4A", sceAgcSetShRegIndirectPatchSetAddress_nid_no_patch); + +int APS5_VABI sceAgcDcbJump_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("xSAR0LTcRKM", sceAgcDcbJump_nid_no_patch); + +int APS5_VABI sceAgcDcbJumpGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("VEGu4dixjUg", sceAgcDcbJumpGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbPrimeUtcl2_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("szG7hz2yEhA", sceAgcAcbPrimeUtcl2_nid_no_patch); + +int APS5_VABI sceAgcDcbSetCxRegisterDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("1DeUNpRIDDA", sceAgcDcbSetCxRegisterDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcQueueEndOfPipeActionPatchType_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("T9fjQIINoeE", sceAgcQueueEndOfPipeActionPatchType_nid_no_patch); + +int APS5_VABI sceAgcDcbSetCxRegisterDirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("LHFXRrlTPD8", sceAgcDcbSetCxRegisterDirect_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndex_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("q88lQ+GP5Yk", sceAgcDcbDrawIndex_nid_no_patch); + +int APS5_VABI sceAgcCbDispatchGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("Abendgtz+3o", sceAgcCbDispatchGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbGetLodStatsGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("rUuVjyR+Rd4", sceAgcDcbGetLodStatsGetSize_nid_no_patch); + +int APS5_VABI sceAgcGetDataPacketPayloadAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("CQsSq6l6+kA", sceAgcGetDataPacketPayloadAddress_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndirect_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("1q1titRBL6o", sceAgcDcbDrawIndirect_nid_no_patch); + +int APS5_VABI sceAgcAcbSetFlip_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ebixW91gpPw", sceAgcAcbSetFlip_nid_no_patch); + +int APS5_VABI sceAgcGetFusedShaderSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("nQT5kYLv0cg", sceAgcGetFusedShaderSize_nid_no_patch); + +int APS5_VABI sceAgcSetRangePredication_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("n8vgpaQg6dA", sceAgcSetRangePredication_nid_no_patch); + +int APS5_VABI sceAgcDcbSetShRegisterDirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("QhPDD513V0w", sceAgcDcbSetShRegisterDirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbSetIndexSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("GIIW2J37e70", sceAgcDcbSetIndexSize_nid_no_patch); + +int APS5_VABI sceAgcSetPacketPredication_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("w6Dj1VJt5qY", sceAgcSetPacketPredication_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexOffset_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("B+aG9DUnTKA", sceAgcDcbDrawIndexOffset_nid_no_patch); + +int APS5_VABI sceAgcAsyncRewindPatchSetRewindState_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("eWaWyFegzgQ", sceAgcAsyncRewindPatchSetRewindState_nid_no_patch); + +int APS5_VABI sceAgcJumpPatchSetTarget_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("2BS4EtAaF28", sceAgcJumpPatchSetTarget_nid_no_patch); + +int APS5_VABI sceAgcBranchPatchSetCompareAddress_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("GXBlM-ekzrI", sceAgcBranchPatchSetCompareAddress_nid_no_patch); + +int APS5_VABI sceAgcAcbJumpGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("b-oySn+G2tE", sceAgcAcbJumpGetSize_nid_no_patch); + +int APS5_VABI sceAgcCbBranch_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("w1KFAHVqpaU", sceAgcCbBranch_nid_no_patch); + +int APS5_VABI sceAgcDcbDrawIndexIndirectGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("mStuvI0zOtc", sceAgcDcbDrawIndexIndirectGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbCopyData_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("qzMN2XKGA4k", sceAgcAcbCopyData_nid_no_patch); + +int APS5_VABI sceAgcDcbWriteDataGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("p9tI+yTvx68", sceAgcDcbWriteDataGetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbAtomicMem_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("XKKuA6VkSRc", sceAgcAcbAtomicMem_nid_no_patch); + +int APS5_VABI sceAgcAcbPrimeUtcl2GetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("eCjKaqeeQ5s", sceAgcAcbPrimeUtcl2GetSize_nid_no_patch); + +int APS5_VABI sceAgcAcbResetQueue_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("JrtiDtKeS38", sceAgcAcbResetQueue_nid_no_patch); + +int APS5_VABI sceAgcAcbQueueEndOfShaderActionGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("F8NLhWvFemI", sceAgcAcbQueueEndOfShaderActionGetSize_nid_no_patch); + +int APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize_nid_no_patch(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} +APS5_EXPORT("ms1xVoZ-Vwc", sceAgcDcbBeginOcclusionQueryGetSize_nid_no_patch); } diff --git a/docs/dev/debt/libSceAgcVsh.md b/docs/dev/debt/libSceAgcVsh.md new file mode 100644 index 0000000000..c2fb6a18d8 --- /dev/null +++ b/docs/dev/debt/libSceAgcVsh.md @@ -0,0 +1,5 @@ +# libSceAgcVsh technical debt + +### Unknown function info + +- The 150 declarations in [Export.cpp](../../../core/libs/prx/libSceAgcVsh/Export.cpp) are present in the PS5 3.20 [export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceAgcVsh.c) and the local import-stub table. Their signatures and behavior are unverified; the parameterless declarations are for import resolution only and throw through NotImplemented_nid_no_patch. From 0f35e7b1760378415d4c106ddb222609b397e8e1 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:35:47 +0000 Subject: [PATCH 192/318] fix(libc): follow msun for coshf --- core/libs/prx/libc/src/Math.cpp | 24 +++++++++++++++++++++++- docs/dev/debt/libc.md | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index 37616b7be3..b8fdda2b22 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -487,7 +487,29 @@ double APS5_VABI sin_nid_postfix(double x) { return std::sin(x); } double APS5_VABI cos_nid_postfix(double x) { return std::cos(x); } double APS5_VABI sinh_nid_postfix(double x) { return std::sinh(x); } double APS5_VABI cosh_nid_postfix(double x) { return std::cosh(x); } -float APS5_VABI coshf_nid_postfix(float x) { return std::cosh(x); } +float APS5_VABI coshf_nid_postfix(float x) { + const std::uint32_t magnitude = std::bit_cast(x) & 0x7fffffffu; + if (magnitude >= 0x7f800000u) return x * x; + const float absolute = std::fabs(x); + if (magnitude < 0x3eb17218u) { + const float t = SinhfExpm1(absolute); + const float w = 1.0f + t; + if (magnitude < 0x39800000u) return 1.0f; + return 1.0f + (t * t) / (w + w); + } + if (magnitude < 0x41100000u) { + const float t = SinhfExp(absolute); + return 0.5f * t + 0.5f / t; + } + if (magnitude < 0x42b17217u) return 0.5f * SinhfExp(absolute); + if (magnitude <= 0x42b2d4fcu) { + const float w = SinhfExp(0.5f * absolute); + const float t = 0.5f * w; + return t * w; + } + volatile float huge = 1.0e30f; + return huge * huge; +} float APS5_VABI sinhf_nid_postfix(float x) { const std::uint32_t magnitude = std::bit_cast(x) & 0x7fffffffu; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index c9cda8fa5a..1d28b3280a 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -85,6 +85,7 @@ - [jrand48 and mrand48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) return the high 32 bits of the new 48-bit state zero-extended to 64 bits, as the console's `libSceLibcInternal.sprx` does when called from a homebrew payload process on a PS5 (firmware 12.70): a result with bit 31 set comes back between 0x80000000 and 0xFFFFFFFF, as FreeBSD 11.0's [`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/jrand48.c#L23) and [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/lib/libc/gen/mrand48.c#L25) return it; FreeBSD 11.1 and later cast it to `int32_t` ([`jrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/jrand48.c#L26), [`mrand48`](https://github.com/freebsd/freebsd-src/blob/release/11.1.0/lib/libc/gen/mrand48.c#L28)), which makes it negative. `lrand48` and `nrand48` matched FreeBSD there. Whether a title's own libc does the same is unverified - [seed48 and lcong48](../../../core/libs/prx/libc/src/Random.cpp) (libSceLibcInternal, implemented in libc) follow the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x6F460 and 0x6F420): the multiplier and addend are shared by the whole `rand48` family, including `erand48`, `nrand48` and `jrand48` on a caller's seed; `lcong48` sets them with the seed, and `srand48` and `seed48` restore 0x5DEECE66D and 0xB. Unlike FreeBSD, `seed48` returns a static copy of the new seed rather than the previous one. A null argument, which the console dereferences, throws - [lgamma and signgam](../../../core/libs/prx/libc/src/Math.cpp) (libc) - `lgamma` returns the host's `lgamma` and stores the sign of Γ(x) in the exported `signgam` (NID `5gOOC0kzW0c`, starting at 0 as in FreeBSD 11's [`s_signgam.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/s_signgam.c)) by the rules of [`e_lgamma_r.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/msun/src/e_lgamma_r.c): -1 for -0 and for a negative non-integer whose floor is odd, 1 otherwise, including poles, infinities and NaN. Taken from the FreeBSD source, not measured on the console. The NID is in the aerolib symbol list; no title is known to read `signgam`. PPSA99004 imports `lgamma` +- [coshf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows FreeBSD msun's [`e_coshf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_coshf.c), using the `expm1f` and `expf` paths already inlined for `sinhf` below; the Windows UCRT differed from the `libc.prx` copies in PPSA07923 and PPSA13934 on 500,554 and 500,614 of 20 million inputs in the #9707 survey. The exact system `libSceLibcInternal` behavior and the FMA-contracted `libc.prx` results remain unverified - [sinhf](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `sinhf` of the `libc.prx` bundled with PPSA28319 (disassembly): FreeBSD msun's [`e_sinhf.c`](https://github.com/freebsd/freebsd-src/blob/5aa554c7e5b6fd9042986df07f12ac5687e41392/lib/msun/src/e_sinhf.c) from before `__ldexp_expf`, which squares `expf(|x| / 2)` between `logf(FLT_MAX)` and the overflow threshold, calling msun's [`expm1f`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/s_expm1f.c) and [`expf`](https://github.com/freebsd/freebsd-src/blob/59bc2f095289a10b8cfa55cbb2b45c3bd6c2d4ae/lib/msun/src/e_expf.c), whose paths for the arguments it passes are inlined here. Past the threshold Sony's error helper sets `errno` to `ERANGE` before returning `x * 1e37`; msun itself sets no `errno`. That code, run on a Windows host, gives the same bits as this implementation for every finite argument up to the threshold, of both signs; it was not run on Linux. That the system `libSceLibcInternal`, which Ruffle (PPSA68091) and RetroArch (PPSA99169) import while bundling a zero-return `libc.prx`, has the same code is assumed - [acoshl, asinhl, atanhl, atan2l, cbrtl, erfl, erfcl, expm1l, hypotl, log1pl, log2l, lgammal, tgammal, j0f, j1f, jnf, y0f, y1f and ynf](../../../core/libs/prx/libc/src/Math.cpp) (libc) return the host C library's results (the float Bessel functions through its double ones), not those of the console's own code, so the last bits can differ (`y0f`, `y1f`, `ynf` follow the console at `+-0`). As in the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x56410), `lgammal` does not store the sign in `signgam`; `hypotl` returns infinity for an infinite argument even when the other is a NaN, as C11 F.10.4.3 requires. [hypot3, hypot3f and hypot3l](../../../core/libs/prx/libc/src/Math.cpp) follow that module (0x51D30, 0x520D0, 0x52420): the first NaN of x, y and z is returned unchanged, any infinity gives positive infinity, a zero argument gives the two-argument hypot of the other two, and otherwise the largest magnitude is combined with the hypot of the other two - [asinl](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows the `asinl` of the `libc.prx` bundled with PPSA28319 (disassembly), FreeBSD msun's [`e_asinl.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/src/e_asinl.c) with the ld80 [`invtrig.c`](https://github.com/freebsd/freebsd-src/blob/9f0432041b8fb69832f10c32da50a67f34feeb61/lib/msun/ld80/invtrig.c) coefficients, which the binary holds as the same 80-bit constants. For |x| > 1, the infinities and NaN, Sony's error helper raises `FE_INVALID` and sets `errno` to `EDOM` before returning `(x - x) / (x - x)`, so a NaN argument also sets `EDOM`; msun itself sets no `errno`. 20 million random arguments in (-1, 1) give the same bits as that code run on a Windows host. The result needs the x87 precision control at 64 bits, as FreeBSD amd64 starts threads (control word `0x37f`), but guest threads do not set it: on Windows a thread started by `_beginthreadex` runs at 53 bits (`0x27f`), where `asinl(0.75)` returns mantissa `0xd91a98ae3406e000` instead of `0xd91a98ae3406e041`. The system `libSceLibcInternal` that RetroArch (PPSA99169) imports is assumed to have the same code From af343b1601385e2466e621f2c90dff6636bd8a04 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 03:35:47 +0000 Subject: [PATCH 193/318] test(libc): cover coshf range boundaries --- core/libs/tests/GuestMath.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index 04541e7c79..4591f77277 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -868,6 +868,37 @@ static void CheckHyperbolic() { Require(Near(acosh_nid_postfix(1e300), 691.4686750787736, 1e-15)); } +static void CheckCoshfMsunBoundaries() { + static constexpr struct { + std::uint32_t input; + std::uint32_t output; + } cases[] = { + {0x00000000u, 0x3f800000u}, + {0x80000000u, 0x3f800000u}, + {0x00000001u, 0x3f800000u}, + {0x397fffffu, 0x3f800000u}, + {0x39800000u, 0x3f800000u}, + {0x3eb17217u, 0x3f87c3b6u}, + {0x3eb17218u, 0x3f87c3b6u}, + {0x3f800000u, 0x3fc583aau}, + {0xbf800000u, 0x3fc583aau}, + {0x410fffffu, 0x457d389cu}, + {0x41100000u, 0x457d38acu}, + {0x42b17216u, 0x7effff04u}, + {0x42b17217u, 0x7effff84u}, + {0x42b17218u, 0x7f000002u}, + {0x42b2d4fbu, 0x7f7fff6cu}, + {0x42b2d4fcu, 0x7f7fffecu}, + {0xc2b2d4fcu, 0x7f7fffecu}, + }; + for (const auto& test : cases) + Require(std::bit_cast(coshf_nid_postfix(std::bit_cast(test.input))) == test.output); + Require(std::bit_cast(coshf_nid_postfix(std::bit_cast(0x42b2d4fdu))) == 0x7f800000u); + Require(std::bit_cast(coshf_nid_postfix(INFINITY)) == 0x7f800000u); + Require(std::bit_cast(coshf_nid_postfix(-INFINITY)) == 0x7f800000u); + Require(std::bit_cast(coshf_nid_postfix(std::bit_cast(0x7fc12345u))) == 0x7fc12345u); +} + static void CheckScaledHyperbolic() { const int rounding = std::fegetround(); Require(std::fesetround(FE_TONEAREST) == 0); @@ -1586,6 +1617,7 @@ int main() { Require(std::isinf(__powidf2_nid_postfix(-0.0, -3)) && std::signbit(__powidf2_nid_postfix(-0.0, -3))); Require(__powidf2_nid_postfix(2.0, -127) == 0x1p-127); CheckHyperbolic(); + CheckCoshfMsunBoundaries(); CheckScaledHyperbolic(); CheckSinhf(); CheckAtanhf(); From 0a3ea213e16a691724efcb8ae8a12d083f081b28 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Wed, 21 Oct 2026 01:30:13 +0700 Subject: [PATCH 194/318] fix(shader): read a 2D MSAA T# through DIM 2D as its sample 0 A DIM 2D image_load or image_sample of a 2D MSAA T# returns sample 0 on RDNA2 (RX 6700 XT, measured through HIP; bilinear blends sample-0 texels). The recompiler now reads such a direct sampled access as a 2D image, and the driver binds a one-sample copy of the target's sample 0, refreshed by a fragment pass before each binding. Peace Walker (PPSA18216) stops on this read at the end of its opening movie. Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 21 +- .../Graphics/include/FirstSampleCopy.hpp | 40 +++ .../Graphics/include/MultisampleTarget.hpp | 1 + .../Graphics/include/Recorder.hpp | 2 + .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/shaders/FirstSample.frag | 16 ++ .../Graphics/src/FirstSampleCopy.cpp | 252 ++++++++++++++++++ .../Graphics/src/MultisampleTarget.cpp | 41 +++ .../libSceAgcDriver/Graphics/src/Recorder.cpp | 24 +- .../Graphics/src/ShaderResources.cpp | 12 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 95 +++++-- .../libSceAgcDriver/tests/ShaderMemory.cpp | 32 +++ .../execution/SampleMultisampleTarget.cpp | 128 ++++++++- .../Optimization/src/ResourceMaterializer.cpp | 5 + docs/dev/debt/libSceAgcDriver.md | 1 + docs/dev/debt/shader.md | 1 + 16 files changed, 629 insertions(+), 43 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/FirstSample.frag create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7902207afd..cccc8f8739 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -225,6 +225,22 @@ foreach(agcSampleOrderVariant IN ITEMS "R8;comp;-DTEXEL_FORMAT=r8ui" "R16;comp;- ) list(APPEND agcSampleOrderShaderHeaders ${agcSampleOrderShaderHeader}) endforeach() +foreach(agcFirstSampleClass IN ITEMS Float Uint Sint) + string(TOUPPER "${agcFirstSampleClass}" agcFirstSampleSymbol) + set(agcFirstSampleShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/FirstSample.frag) + set(agcFirstSampleShaderSpv ${agcTextureDetileGeneratedDir}/FirstSample${agcFirstSampleClass}.spv) + set(agcFirstSampleShaderHeader ${agcTextureDetileGeneratedDir}/FirstSample${agcFirstSampleClass}_spv.h) + add_custom_command( + OUTPUT "${agcFirstSampleShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -D${agcFirstSampleSymbol}_SAMPLES=1 -o "${agcFirstSampleShaderSpv}" "${agcFirstSampleShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcFirstSampleShaderSpv} -DOUTPUT=${agcFirstSampleShaderHeader} + -DSYMBOL=FIRST_SAMPLE_${agcFirstSampleSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcFirstSampleShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcPresentationShaderHeaders ${agcFirstSampleShaderHeader}) +endforeach() foreach(agcOverlayStage IN ITEMS Vert Frag) string(TOLOWER "${agcOverlayStage}" agcOverlayExtension) string(TOUPPER "${agcOverlayStage}" agcOverlaySymbol) @@ -323,6 +339,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/FirstSampleCopy.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetLayout.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/ColorTargetTransfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp @@ -1657,10 +1674,12 @@ if(BUILD_TESTING) set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_sample_dumps COMMAND agc_driver_recorder_tests --sample-dumps-only) set_tests_properties(agc_sample_dumps PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_uncounted_passes COMMAND agc_driver_recorder_tests --uncounted-passes-only) + set_tests_properties(agc_uncounted_passes PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) add_test(NAME agc_draw_descriptors COMMAND agc_driver_recorder_tests --draw-descriptors-only) set_tests_properties(agc_draw_descriptors PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) - set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_uncounted_passes agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_driver_host_import_lifetime COMMAND agc_driver_recorder_tests --host-import-lifetime-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp new file mode 100644 index 0000000000..4f973ff75e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp @@ -0,0 +1,40 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_FIRSTSAMPLECOPY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_FIRSTSAMPLECOPY_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" + +namespace AgcDriver::Graphics { + +class Recorder; + +class FirstSampleCopy { +public: + FirstSampleCopy(const Context& context, VkFormat format, VkExtent2D extent); + ~FirstSampleCopy(); + FirstSampleCopy(const FirstSampleCopy&) = delete; + FirstSampleCopy& operator=(const FirstSampleCopy&) = delete; + + void Record(Recorder& recorder, VkImageView source, VkImageLayout layout); + VkImage Image() const { return image; } + +private: + void release() noexcept; + + Context context; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkImageView view = VK_NULL_HANDLE; + VkSampler sampler = VK_NULL_HANDLE; + VkDescriptorSetLayout descriptorLayout = VK_NULL_HANDLE; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkRenderPass renderPass = VK_NULL_HANDLE; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + VkPipeline pipeline = VK_NULL_HANDLE; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp index b9c18eb7d1..68eb4d07db 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp @@ -67,6 +67,7 @@ bool TakeMultisampledSingleClear(std::span code, std::span< bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear); SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target); std::shared_ptr MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D); +std::shared_ptr MultisampleTargetFirstSample(const Context& context, const GuestTextureResource& resource, VkComponentMapping components); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image = nullptr); VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr& owner, VkImage* image = nullptr); void KeepMultisampleTarget_nid_no_patch(const std::shared_ptr& owner); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 6ef4b5cf08..2a569afda2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -220,6 +220,7 @@ public: bool QueuesSampleDumps() const; void PrepareSampleSlot(); void EndPassSamples(); + void RecordUncounted(const std::function& record); struct SampleDumpStatistics { std::uint64_t queued; std::uint64_t queuedInPass; @@ -578,6 +579,7 @@ private: VkQueryPool queries = VK_NULL_HANDLE; VkQueryPool samples = VK_NULL_HANDLE; std::shared_ptr samplePool; + std::vector endedSamples; bool sampleActive = false; bool samplesDrawn = false; VkQueryPool segments = VK_NULL_HANDLE; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 5321104605..fb6569cf46 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -472,6 +472,7 @@ private: std::array words{}; bool depthCompare = false; VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D; + bool firstSample = false; std::shared_ptr texture; }; PreviousSampled previousSampled; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/FirstSample.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/FirstSample.frag new file mode 100644 index 0000000000..287527d25a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/FirstSample.frag @@ -0,0 +1,16 @@ +#version 450 + +#if defined(UINT_SAMPLES) +layout(set = 0, binding = 0) uniform usampler2DMS source; +layout(location = 0) out uvec4 color; +#elif defined(SINT_SAMPLES) +layout(set = 0, binding = 0) uniform isampler2DMS source; +layout(location = 0) out ivec4 color; +#else +layout(set = 0, binding = 0) uniform sampler2DMS source; +layout(location = 0) out vec4 color; +#endif + +void main() { + color = texelFetch(source, ivec2(gl_FragCoord.xy), 0); +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp new file mode 100644 index 0000000000..3a33f8bc0d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp @@ -0,0 +1,252 @@ +#include "prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/FirstSampleFloat_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/FirstSampleSint_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/FirstSampleUint_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/PresentationPassVert_spv.h" +#include +#include + +namespace AgcDriver::Graphics { +namespace { + +bool sintFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_SINT: case VK_FORMAT_R8G8_SINT: case VK_FORMAT_R8G8B8A8_SINT: case VK_FORMAT_B8G8R8A8_SINT: + case VK_FORMAT_A8B8G8R8_SINT_PACK32: case VK_FORMAT_A2B10G10R10_SINT_PACK32: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_SINT: + case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R32_SINT: case VK_FORMAT_R32G32_SINT: case VK_FORMAT_R32G32B32A32_SINT: + return true; + default: return false; + } +} + +bool uintFormat(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_UINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8B8A8_UINT: case VK_FORMAT_B8G8R8A8_UINT: + case VK_FORMAT_A8B8G8R8_UINT_PACK32: case VK_FORMAT_A2R10G10B10_UINT_PACK32: case VK_FORMAT_A2B10G10R10_UINT_PACK32: case VK_FORMAT_R16_UINT: + case VK_FORMAT_R16G16_UINT: case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R32_UINT: case VK_FORMAT_R32G32_UINT: case VK_FORMAT_R32G32B32A32_UINT: + return true; + default: return false; + } +} + +std::span fragmentCode(VkFormat format) { + if (sintFormat(format)) return FIRST_SAMPLE_SINT_SPV; + if (uintFormat(format)) return FIRST_SAMPLE_UINT_SPV; + return FIRST_SAMPLE_FLOAT_SPV; +} + +struct DescriptorPool { + DescriptorPool(const Context& context, VkDescriptorPool pool) : context(context), pool(pool) {} + ~DescriptorPool() { context.Function("vkDestroyDescriptorPool")(context.device, pool, nullptr); } + DescriptorPool(const DescriptorPool&) = delete; + DescriptorPool& operator=(const DescriptorPool&) = delete; + Context context; + VkDescriptorPool pool; +}; + +} + +FirstSampleCopy::FirstSampleCopy(const Context& context, VkFormat format, VkExtent2D extent) : context(context), extent(extent) { + try { + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.format = format; + imageInfo.extent = {extent.width, extent.height, 1}; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage first sample"); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory first sample"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory first sample"); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView first sample"); + + VkSamplerCreateInfo samplerInfo{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + samplerInfo.magFilter = VK_FILTER_NEAREST; + samplerInfo.minFilter = VK_FILTER_NEAREST; + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + Check(context.Function("vkCreateSampler")(context.device, &samplerInfo, nullptr, &sampler), "vkCreateSampler first sample"); + VkDescriptorSetLayoutBinding binding{}; + binding.binding = 0; + binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + binding.descriptorCount = 1; + binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + binding.pImmutableSamplers = &sampler; + VkDescriptorSetLayoutCreateInfo descriptorInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + descriptorInfo.bindingCount = 1; + descriptorInfo.pBindings = &binding; + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &descriptorInfo, nullptr, &descriptorLayout), "vkCreateDescriptorSetLayout first sample"); + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.setLayoutCount = 1; + layoutInfo.pSetLayouts = &descriptorLayout; + Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout first sample"); + + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.finalLayout = VK_IMAGE_LAYOUT_GENERAL; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_GENERAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &reference; + const VkSubpassDependency dependencies[2] = { + {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0}, + {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, 0}, + }; + VkRenderPassCreateInfo renderPassInfo{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + renderPassInfo.attachmentCount = 1; + renderPassInfo.pAttachments = &attachment; + renderPassInfo.subpassCount = 1; + renderPassInfo.pSubpasses = &subpass; + renderPassInfo.dependencyCount = 2; + renderPassInfo.pDependencies = dependencies; + Check(context.Function("vkCreateRenderPass")(context.device, &renderPassInfo, nullptr, &renderPass), "vkCreateRenderPass first sample"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = renderPass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = extent.width; + framebufferInfo.height = extent.height; + framebufferInfo.layers = 1; + Check(context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer), "vkCreateFramebuffer first sample"); + + vertex = CreateShaderModule(context, PRESENTATION_PASS_VERT_SPV, sizeof(PRESENTATION_PASS_VERT_SPV), "vkCreateShaderModule first sample vertex"); + const auto code = fragmentCode(format); + fragment = CreateShaderModule(context, code.data(), code.size_bytes(), "vkCreateShaderModule first sample fragment"); + VkPipelineShaderStageCreateInfo stages[2]{{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}, {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO}}; + stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT; + stages[1].module = fragment; + stages[1].pName = "main"; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const VkViewport viewportRect{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + const VkRect2D scissor{{0, 0}, extent}; + VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport.viewportCount = 1; + viewport.pViewports = &viewportRect; + viewport.scissorCount = 1; + viewport.pScissors = &scissor; + VkPipelineRasterizationStateCreateInfo rasterization{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + rasterization.polygonMode = VK_POLYGON_MODE_FILL; + rasterization.cullMode = VK_CULL_MODE_NONE; + rasterization.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + rasterization.lineWidth = 1.0f; + VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + VkPipelineColorBlendAttachmentState blendAttachment{}; + blendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &blendAttachment; + VkGraphicsPipelineCreateInfo pipelineInfo{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + pipelineInfo.stageCount = 2; + pipelineInfo.pStages = stages; + pipelineInfo.pVertexInputState = &vertexInput; + pipelineInfo.pInputAssemblyState = &assembly; + pipelineInfo.pViewportState = &viewport; + pipelineInfo.pRasterizationState = &rasterization; + pipelineInfo.pMultisampleState = &multisample; + pipelineInfo.pColorBlendState = &blend; + pipelineInfo.layout = pipelineLayout; + pipelineInfo.renderPass = renderPass; + Check(context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines first sample"); + } catch (...) { + release(); + throw; + } +} + +FirstSampleCopy::~FirstSampleCopy() { + release(); +} + +void FirstSampleCopy::release() noexcept { + if (pipeline) context.Function("vkDestroyPipeline")(context.device, pipeline, nullptr); + if (fragment) context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + if (vertex) context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (renderPass) context.Function("vkDestroyRenderPass")(context.device, renderPass, nullptr); + if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + if (descriptorLayout) context.Function("vkDestroyDescriptorSetLayout")(context.device, descriptorLayout, nullptr); + if (sampler) context.Function("vkDestroySampler")(context.device, sampler, nullptr); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + if (image) context.Function("vkDestroyImage")(context.device, image, nullptr); + if (memory) context.Function("vkFreeMemory")(context.device, memory, nullptr); + pipeline = VK_NULL_HANDLE; + fragment = vertex = VK_NULL_HANDLE; + framebuffer = VK_NULL_HANDLE; + renderPass = VK_NULL_HANDLE; + pipelineLayout = VK_NULL_HANDLE; + descriptorLayout = VK_NULL_HANDLE; + sampler = VK_NULL_HANDLE; + view = VK_NULL_HANDLE; + image = VK_NULL_HANDLE; + memory = VK_NULL_HANDLE; +} + +void FirstSampleCopy::Record(Recorder& recorder, VkImageView source, VkImageLayout layout) { + const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1; + poolInfo.poolSizeCount = 1; + poolInfo.pPoolSizes = &size; + VkDescriptorPool created = VK_NULL_HANDLE; + Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &created), "vkCreateDescriptorPool first sample"); + const auto pool = std::make_shared(context, created); + VkDescriptorSetAllocateInfo allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + allocation.descriptorPool = pool->pool; + allocation.descriptorSetCount = 1; + allocation.pSetLayouts = &descriptorLayout; + VkDescriptorSet set = VK_NULL_HANDLE; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &allocation, &set), "vkAllocateDescriptorSets first sample"); + const VkDescriptorImageInfo imageInfo{VK_NULL_HANDLE, source, layout}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = set; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; + write.pImageInfo = &imageInfo; + context.Function("vkUpdateDescriptorSets")(context.device, 1, &write, 0, nullptr); + recorder.Keep(pool); + recorder.RecordUncounted([&](VkCommandBuffer commands) { + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = renderPass; + begin.framebuffer = framebuffer; + begin.renderArea = {{0, 0}, extent}; + context.Resolved(&DeviceFunctions::cmdBeginRenderPass, "vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + context.Function("vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &set, 0, nullptr); + context.Function("vkCmdDraw")(commands, 3, 1, 0, 0); + context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands); + }); +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp index 809e680d42..b2fa994818 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/FirstSampleCopy.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" @@ -426,6 +427,19 @@ public: return {shared_from_this(), texture.get()}; } + std::shared_ptr FirstSample(VkComponentMapping components) { + const auto source = Sampled({}); + if (firstSampleCopy == nullptr) firstSampleCopy = std::make_unique(context, target.format, target.extent); + firstSampleCopy->Record(*Recorder::Active(), source->View(), source->Layout()); + const std::array key{static_cast(components.r), static_cast(components.g), static_cast(components.b), static_cast(components.a)}; + auto& texture = firstSampleTextures[key]; + if (texture == nullptr) { + texture = std::make_shared(context, firstSampleCopy->Image(), target.format, VK_IMAGE_ASPECT_COLOR_BIT, components); + texture->MarkRefreshedPerUse(); + } + return {shared_from_this(), texture.get()}; + } + VkImageView View(VkFormat format) { if (format == target.format) return view; Require(twinFormat != VK_FORMAT_UNDEFINED && format == twinFormat, "a multisampled color target is only viewed in its own format or its sRGB twin"); @@ -516,6 +530,8 @@ private: std::map, std::unique_ptr> fmaskTextures; std::map, std::shared_ptr> textures; std::vector> retiredTextures; + std::unique_ptr firstSampleCopy; + std::map, std::shared_ptr> firstSampleTextures; std::vector dccRanges; std::optional dccFill; std::optional cmaskFill; @@ -626,6 +642,8 @@ private: fmaskTextures.clear(); textures.clear(); retiredTextures.clear(); + firstSampleTextures.clear(); + firstSampleCopy.reset(); for (const auto& [format, resolvePass] : resolvePasses) context.Function("vkDestroyRenderPass")(context.device, resolvePass, nullptr); if (orderView) context.Function("vkDestroyImageView")(context.device, orderView, nullptr); if (twinView) context.Function("vkDestroyImageView")(context.device, twinView, nullptr); @@ -1159,6 +1177,29 @@ std::shared_ptr MultisampleTargetTexture(const Context& context, const throw std::runtime_error(text); } +std::shared_ptr MultisampleTargetFirstSample(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) { + std::lock_guard lock(targetsMutex()); + discardRetiredTargets(); + const auto format = ResolveTextureFormat(resource.format); + const MultisampleTarget* other = nullptr; + for (const auto& target : targets()) { + if (!target->active->load(std::memory_order_acquire)) continue; + if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue; + const auto& rendered = target->target; + const bool sameShape = rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0; + if (sameShape && rendered.format == format) return target->FirstSample(components); + if (sameShape && redBlueSwapped(rendered.format, format)) return target->FirstSample(swapRedBlue(components)); + other = target.get(); + } + if (other == nullptr) { + Require(Recorder::Active() != nullptr, "a multisampled texture is only sampled by recorded draws and dispatches"); + return createFilled(context, resource, format, "sampling").FirstSample(components); + } + char text[320]; + std::snprintf(text, sizeof(text), "AGC graphics: reading sample 0 of multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast(format)); + throw std::runtime_error(text); +} + void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target) { std::lock_guard lock(targetsMutex()); acquire(context, target); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index 9f03343a67..4f1fca0558 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2282,6 +2282,20 @@ void Recorder::EndPassSamples() { if (open != nullptr && open->segmentActive) endSegment(*open); } +void Recorder::RecordUncounted(const std::function& record) { + const auto commands = Commands(); + if (open->segmentActive) endSegment(*open); + const bool counted = open->sampleActive; + if (counted) { + open->endedSamples.reserve(open->endedSamples.size() + 1); + endSamples(*open); + open->endedSamples.push_back(open->samples); + open->samples = VK_NULL_HANDLE; + } + record(commands); + if (counted) beginSamples(*open); +} + void Recorder::takeSegmentPool(Batch& batch) { auto cache = std::static_pointer_cast(segmentPools); if (cache == nullptr) { @@ -2572,9 +2586,15 @@ void Recorder::beginSamples(Batch& batch) { void Recorder::readSamples(Batch& batch) { if (sampleCounterState > 0) return; if (batch.samples == VK_NULL_HANDLE || !batch.submitted) return; + std::uint64_t total = 0; + for (const auto pool : batch.endedSamples) { + std::uint64_t samples = 0; + Check(context.Function("vkGetQueryPoolResults")(context.device, pool, 0, 1, sizeof(samples), &samples, sizeof(samples), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults occlusion"); + total += samples; + } std::uint64_t samples = 0; Check(context.Function("vkGetQueryPoolResults")(context.device, batch.samples, 0, 1, sizeof(samples), &samples, sizeof(samples), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults occlusion"); - samplesPassed.fetch_add(samples, std::memory_order_acq_rel); + samplesPassed.fetch_add(total + samples, std::memory_order_acq_rel); } void Recorder::readGpuTiming(Batch& batch) { @@ -4181,6 +4201,8 @@ void Recorder::release(Batch& batch) noexcept { if (batch.samples != VK_NULL_HANDLE && batch.samplePool == nullptr) context.Function("vkDestroyQueryPool")(context.device, batch.samples, nullptr); batch.samples = VK_NULL_HANDLE; batch.samplePool.reset(); + for (const auto pool : batch.endedSamples) context.Function("vkDestroyQueryPool")(context.device, pool, nullptr); + batch.endedSamples.clear(); batch.segments = VK_NULL_HANDLE; batch.segmentPool.reset(); // A completed (or never submitted) batch's objects are kept for reuse: the fence is signaled or diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d8a6109735..ff27f8accb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4658,18 +4658,23 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind const bool depthCompare = !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element); const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && (resource.dimension == TextureDimension::k2DArray || (depthCompare && resource.dimension == TextureDimension::k1DArray)); const bool depth1D = depthCompare && ((binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k1D) || (binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DArray && resource.dimension == TextureDimension::k1DArray)); - if (!firstLayer && !depth1D && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); + const bool firstSample = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DMsaa; + if (!firstLayer && !depth1D && !firstSample && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(resource.dimension)) + ")"); const VkComponentMapping components = ViewComponents(resource); const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes; CompletePendingCpuWrites(context, resource.baseAddress, static_cast(guestBytes)); if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes)); std::shared_ptr texture; const VkImageViewType multisampleViewType = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2DMultisampleArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D; - if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && previousSampled.multisampleViewType == multisampleViewType && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; - if (texture == nullptr && record != nullptr && record->texture != nullptr) { + if (previousSampled.texture != nullptr && previousSampled.depthCompare == depthCompare && previousSampled.multisampleViewType == multisampleViewType && previousSampled.firstSample == firstSample && words.size() == 8 && std::equal(words.begin(), words.end(), previousSampled.words.begin())) texture = previousSampled.texture; + if (texture == nullptr && !firstSample && record != nullptr && record->texture != nullptr) { texture = fastTexture(*record); (texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed); } + if (texture == nullptr && firstSample) { + Require(!depthCompare && !DepthSurfaceAt(resource.baseAddress), "reading sample 0 of a multisampled depth surface through a 2D image is not implemented"); + texture = MultisampleTargetFirstSample(context, resource, components); + } if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, depthCompare, multisampleViewType); if (resource.dimension == TextureDimension::k2DMsaa) multisampleSources.push_back(resource.baseAddress); bool baseLevel = false; @@ -4697,6 +4702,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind std::copy(words.begin(), words.end(), previousSampled.words.begin()); previousSampled.depthCompare = depthCompare; previousSampled.multisampleViewType = multisampleViewType; + previousSampled.firstSample = firstSample; previousSampled.texture = texture; } textures.push_back(std::move(texture)); diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 8a0b4d0ddc..21270e8d0e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -7032,35 +7032,7 @@ public: const auto commands = start.commands; if (!continued) { recorder.PrepareSampleSlot(); - if (context.graphicsPipelineLibrary) { - if (!transitioned) { - transitioned = true; - VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; - barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; - barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; - barrier.image = target.Image(); - barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; - context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); - } - VkRenderingAttachmentInfoKHR attachment{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR}; - attachment.imageView = target.View(); - attachment.imageLayout = VK_IMAGE_LAYOUT_GENERAL; - attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - VkRenderingInfoKHR rendering{VK_STRUCTURE_TYPE_RENDERING_INFO_KHR}; - rendering.renderArea = {{0, 0}, {8, 8}}; - rendering.layerCount = 1; - rendering.colorAttachmentCount = 1; - rendering.pColorAttachments = &attachment; - context.Resolved(&DeviceFunctions::cmdBeginRendering, "vkCmdBeginRenderingKHR")(commands, &rendering); - } else { - VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; - begin.renderPass = renderPass; - begin.framebuffer = framebuffer; - begin.renderArea = {{0, 0}, {8, 8}}; - context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); - } + begin(commands); } context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); recorder.NoteSampledDraw(commands); @@ -7069,10 +7041,48 @@ public: return continued; } + void Record(VkCommandBuffer commands) const { + begin(commands); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + context.Function("vkCmdDraw")(commands, 3, 1, 0, 0); + EndRenderPass(context, commands); + } + static constexpr std::uint64_t Samples = 64; private: static constexpr std::uint64_t PassKey = 0xc0de; + void begin(VkCommandBuffer commands) const { + if (context.graphicsPipelineLibrary) { + if (!transitioned) { + transitioned = true; + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL; + barrier.image = target.Image(); + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + } + VkRenderingAttachmentInfoKHR attachment{VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR}; + attachment.imageView = target.View(); + attachment.imageLayout = VK_IMAGE_LAYOUT_GENERAL; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + VkRenderingInfoKHR rendering{VK_STRUCTURE_TYPE_RENDERING_INFO_KHR}; + rendering.renderArea = {{0, 0}, {8, 8}}; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + context.Resolved(&DeviceFunctions::cmdBeginRendering, "vkCmdBeginRenderingKHR")(commands, &rendering); + } else { + VkRenderPassBeginInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + info.renderPass = renderPass; + info.framebuffer = framebuffer; + info.renderArea = {{0, 0}, {8, 8}}; + context.Function("vkCmdBeginRenderPass")(commands, &info, VK_SUBPASS_CONTENTS_INLINE); + } + } void release() noexcept { const auto device = context.device; if (pipeline != VK_NULL_HANDLE) context.Function("vkDestroyPipeline")(device, pipeline, nullptr); @@ -7420,6 +7430,24 @@ void sampleDumpTests(const Device& device, Recorder& recorder) { inPassSampleDumpTests(recorder, context, cover, words, address, target); } +void uncountedPassTests(const Device& device) { + auto context = device.GetContext(); + context.bufferDeviceAddress = false; + Recorder recorder(context); + CoverPass cover(context); + recorder.CountSamples(); + Require(!recorder.QueuesSampleDumps(), "a recorder without buffer device addresses counted samples in segments"); + const auto base = recorder.SamplesTotal(); + cover.Draw(recorder); + recorder.RecordUncounted([&](VkCommandBuffer commands) { cover.Record(commands); }); + cover.Draw(recorder); + recorder.Submit(); + recorder.Sync(); + const auto counted = recorder.SamplesTotal() - base; + Require(counted != 0, "the draws around an uncounted pass were not counted"); + if (context.occlusionQueryPrecise) Require(counted == 2 * CoverPass::Samples, "the batch-wide occlusion query counted " + std::to_string(counted) + " samples, not the " + std::to_string(2 * CoverPass::Samples) + " of the two draws around an uncounted pass"); +} + void cmaskPassTests(const Device& device, Recorder& recorder) { namespace GuestMemory = AgcDriver::GuestMemory; auto context = device.GetContext(); @@ -10405,6 +10433,15 @@ int main(int argc, char** argv) { std::cout << "Occlusion counter dump tests passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--uncounted-passes-only") { + if (!device.Graphics()) { + std::cout << "skipped, the test queue has no graphics\n"; + return 77; + } + uncountedPassTests(device); + std::cout << "Uncounted pass tests passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--shadowed-key-stores-only") { if (!shadowedKeyStoreTests(device, recorder)) { std::cout << "skipped, the device imports no watched host memory under a unit shadow\n"; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 8d8b12086e..a96499a786 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -7544,6 +7544,37 @@ void verifyVolumeArrayLoadMode() { expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(store, descriptor, ResourceMaterializer::RuntimeImageModes(store))); }, "incompatible with the static runtime image interface", "2D-array image atomic on a 3D T# was accepted"); } +void verifyMultisamplePlaneReadMode() { + using namespace ShaderRecompiler; + const DescriptorValue descriptor{{0x02754d00u, 0xc3800000u, 0x00cbc167u, 0xe1b20facu, 0u, 0x00700020u, 0u, 0u}, 8u}; + ImageResource image{}; + image.resourceClass = ImageResourceClass::Sampled; + image.dimension = RdnaImageDimension::Dim2D; + const auto modes = ResourceMaterializer::RuntimeImageModes(image); + const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(modes[selected].dimension == RdnaImageDimension::Dim2D && !modes[selected].cube && modes[selected].numericClass == IrTextureNumericClass::Float, "a 2D read of a 2D MSAA T# (PPSA18216's) did not select a 2D runtime mode"); + for (const auto& [word1, word3, word5, numericClass] : std::array, 4>{{{0xc0100000u, 0xe1b20facu, 0x00700020u, IrTextureNumericClass::Float}, {0xc0500000u, 0xe1810facu, 0x00700010u, IrTextureNumericClass::Uint}, {0xc0c00000u, 0xe1b20facu, 0x00700020u, IrTextureNumericClass::Sint}, {0xc0d00000u, 0xe1830facu, 0x00700030u, IrTextureNumericClass::Float}}}) { + const DescriptorValue narrow{{0x02754d00u, word1, 0x00cbc167u, word3, 0u, word5, 0u, 0u}, 8u}; + const auto mode = ResourceMaterializer::RuntimeImageMode(image, narrow, modes); + require(modes[mode].dimension == RdnaImageDimension::Dim2D && !modes[mode].cube && modes[mode].numericClass == numericClass, "a 2D read of a 2D MSAA T# with 8-bit or 16-bit texels did not select a 2D runtime mode"); + } + const DescriptorValue depthBits{{0x02754d00u, 0xc0b00000u, 0x00cbc167u, 0xe1820facu, 0u, 0x00700020u, 0u, 0u}, 8u}; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, depthBits, modes)); }, "incompatible with the static runtime image interface", "a 2D read of a 16_UINT SW_64KB_Z_X 2D MSAA T#, read as depth bits, was accepted"); + const auto refused = [&](const ImageResource& access, const char* what) { + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(access, descriptor, ResourceMaterializer::RuntimeImageModes(access))); }, "incompatible with the static runtime image interface", what); + }; + auto other = image; + other.cubeFaceCompatible = false; + refused(other, "a 2D read of a 2D MSAA T# through a mip, gather, bias, gradient or offset access was accepted"); + other = image; + other.depthCompare = true; + refused(other, "a 2D depth comparison of a 2D MSAA T# was accepted"); + other = image; + other.resourceClass = ImageResourceClass::Storage; + other.written = true; + refused(other, "a 2D storage access to a 2D MSAA T# was accepted"); +} + void verifyVolumeBiasTableRejection() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -7799,6 +7830,7 @@ int main(int argc, char** argv) { verifyCubeDepthCompareMode(); verifyVolumeArrayLoadMode(); verifyArrayVolumeStoreMode(); + verifyMultisamplePlaneReadMode(); verifyVolumeBiasTableRejection(); verifyDynamicStorageMipChain(); verifySrgbDepthCompareModes(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp index abe81b5503..69392bd173 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampleMultisampleTarget.cpp @@ -62,9 +62,13 @@ constexpr std::uint32_t Float16x4Info = 0x0730u; constexpr std::uint32_t MultisampledAttrib = (SampleShift << 12u) | (SampleShift << 15u); constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u; constexpr std::uint32_t Float32Export = 9; +constexpr std::uint32_t Uint16AbgrExport = 7; constexpr std::uint32_t Format16161616Float = 71; constexpr std::uint32_t Unorm8x4SwappedInfo = 0x8828u; +constexpr std::uint32_t Uint8x4Info = 0x8428u; constexpr std::uint32_t Format8888Unorm = 56; +constexpr std::uint32_t Format8888Uint = 60; +constexpr std::uint32_t Format8888Sint = 61; constexpr std::uint32_t Type2DMsaa = 14; constexpr std::uint32_t TileRenderTarget = 0x1b; constexpr std::uint32_t TileZ64KBX = 0x18; @@ -79,6 +83,7 @@ constexpr std::uint32_t FillOnes = 0x3c003c00u; constexpr std::array Ones{1.0f, 1.0f, 1.0f, 1.0f}; constexpr std::array Written{0.75f, 0.25f, 0.5f, 1.0f}; constexpr std::uint32_t StoredSample = 1; +constexpr std::array PointMipSampler{0u, 0xfffu << 12u, 1u << 26u, 0u}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, @@ -91,6 +96,12 @@ constexpr std::array DrawnUnorm8{64.0f / 255.0f, 128.0f / 255.0f, 32.0 alignas(256) constexpr std::array Unorm8PixelCode{ 0x7e0802ff, std::bit_cast(DrawnUnorm8[0]), 0x7e0a02ff, std::bit_cast(DrawnUnorm8[1]), 0x7e0c02ff, std::bit_cast(DrawnUnorm8[2]), 0x7e0e02ff, std::bit_cast(DrawnUnorm8[3]), 0xf800180f, 0x07060504, 0xbf810000, }; +constexpr std::array DrawnUint8{0x12u, 0xbeu, 0x42u, 0x81u}; +alignas(256) constexpr std::array Uint8PixelCode{ + 0x7e0802ff, DrawnUint8[0] | (DrawnUint8[1] << 16u), 0x7e0a02ff, DrawnUint8[2] | (DrawnUint8[3] << 16u), 0xf8001c0f, 0x00000504, 0xbf810000, +}; +constexpr std::uint32_t FilledSint8 = 0x81beff12u; +constexpr std::array FilledSint8Texel{0x12u, 0xffffffffu, 0xffffffbeu, 0xffffff81u}; alignas(256) constexpr std::array LoadCode{ 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, @@ -106,6 +117,15 @@ alignas(256) constexpr std::array ArrayLoadCode{ 0x80001301, 0xbf810000, }; +alignas(256) constexpr std::array PlaneCode{ + 0x34020087, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xe0301008, 0x80000401, 0xe030100c, + 0x80000501, 0xe0301010, 0x80000601, 0xbf8c3f70, 0xf0001f08, 0x00011005, 0xf09c0f08, 0x00611402, + 0xf0900f08, 0x00611802, 0xbf8c3f70, 0xe0701030, 0x80001001, 0xe0701034, 0x80001101, 0xe0701038, + 0x80001201, 0xe070103c, 0x80001301, 0xe0701040, 0x80001401, 0xe0701044, 0x80001501, 0xe0701048, + 0x80001601, 0xe070104c, 0x80001701, 0xe0701050, 0x80001801, 0xe0701054, 0x80001901, 0xe0701058, + 0x80001a01, 0xe070105c, 0x80001b01, 0xbf810000, +}; + alignas(256) constexpr std::array StoreCode{ 0x34020087, 0xe030100c, 0x80000501, 0xe0301010, 0x80000601, 0xe0301014, 0x80000701, 0xe0301018, 0x80001001, 0xe030101c, 0x80001101, 0xe0301020, 0x80001201, 0xe0301024, 0x80001301, 0xbf8c3f70, @@ -210,6 +230,21 @@ AgcDriver::Graphics::ColorTarget DecodeSwappedTarget(const Block& block) { return color; } +AgcDriver::Graphics::ColorTarget DecodeUintTarget(const Block& block) { + AgcDriver::Registers cx; + cx[0x318] = static_cast(block.Address() >> 8u); + cx[0x31b] = 0; + cx[0x31c] = Uint8x4Info; + cx[0x31d] = MultisampledAttrib; + cx[0x3b0] = ((Side - 1u) << 14u) | (Side - 1u); + cx[0x3b8] = TiledAttrib3; + cx[0x390] = static_cast(block.Address() >> 40u); + auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0); + Require(color.format == VK_FORMAT_R8G8B8A8_UINT && color.samples == Samples && color.extent.width == Side && color.extent.height == Side, "the 8_8_8_8_UINT target did not decode as a 16x16 RGBA8 UINT surface of 4 samples"); + color.uintExport = true; + return color; +} + void CheckSwappedSamples(const std::vector>& loaded) { const std::array swapped{DrawnUnorm8[2], DrawnUnorm8[1], DrawnUnorm8[0], DrawnUnorm8[3]}; const auto codes = [](const std::array& texel) { @@ -229,6 +264,19 @@ void CheckSwappedSamples(const std::vector>& loaded) { Require(drawn != 0, "no sample of the BGRA8 target holds the drawn color"); } +void CheckUintSamples(const std::vector>& loaded) { + std::size_t drawn = 0; + for (const auto& texel : loaded) { + const auto words = std::bit_cast>(texel); + if (words == std::array{}) continue; + char text[128]; + std::snprintf(text, sizeof(text), "a sample of the 8_8_8_8_UINT target reads %08x %08x %08x %08x instead of the drawn color", words[0], words[1], words[2], words[3]); + Require(words == DrawnUint8, text); + ++drawn; + } + Require(drawn != 0, "no sample of the 8_8_8_8_UINT target holds the drawn color"); +} + AgcDriver::Graphics::State TargetState(const AgcDriver::Graphics::ColorTarget& color) { VkPipelineColorBlendAttachmentState blend{}; blend.colorWriteMask = 0xf; @@ -269,7 +317,7 @@ void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::State& sta const auto vertexResult = ShaderRecompiler::Recompile(vertex); const auto vertexPush = static_cast(vertexResult.pushConstants.size()); const auto codeAddress = reinterpret_cast(pixelCode.data()); - const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, Float32Export}}; + const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, state.color.uintExport ? Uint16AbgrExport : Float32Export}}; const std::array pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}}; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, codeAddress, pixelCode, 0, {}}, @@ -307,12 +355,13 @@ std::array TargetDescriptor(const Block& block, std::uint32_t }; } -ShaderRecompiler::RecompileResult CompileProgram(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture, std::vector& userData) { +ShaderRecompiler::RecompileResult CompileProgram(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture, std::vector& userData, const std::array& sampler = {}) { const auto address = reinterpret_cast(Buffer.data()); const std::array buffer{static_cast(address), static_cast((address >> 32u) & 0xffffu), static_cast(Buffer.size() * 4u), 0x31016facu}; userData.assign(16, 0u); std::copy(buffer.begin(), buffer.end(), userData.begin()); std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 12); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; ShaderRecompiler::RecompileRequest request{ @@ -417,6 +466,37 @@ std::vector> LoadSamples(AgcDriver::VulkanDevice& device, c return LoadSamples(device, TargetDescriptor(block, SampleShift, format, tileMode)); } +void CheckFirstSampleReads(AgcDriver::VulkanDevice& device, const std::array& texture, const std::vector>& samples, bool distinct, const std::string& what) { + std::vector userData; + const auto program = CompileProgram(device, PlaneCode, texture, userData, PointMipSampler); + const auto codeAddress = reinterpret_cast(PlaneCode.data()); + const auto bits = [&](std::size_t index) { return std::bit_cast>(samples[index]); }; + bool differs = false; + for (std::uint32_t first = 0; first < Side * Side; first += Threads) { + Buffer.fill(0xdeadbeefu); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto pixel = first + tid; + const std::array words{std::bit_cast((pixel % Side + 0.5f) / Side), std::bit_cast((pixel / Side + 0.5f) / Side), std::bit_cast(1.0f), pixel % Side, pixel / Side}; + std::copy(words.begin(), words.end(), &Buffer[tid * Words]); + } + device.Dispatch(program, 1, 1, 1, {}, codeAddress); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto pixel = first + tid; + const auto sampleZero = bits(pixel * Samples); + for (std::uint32_t sample = 1; sample < Samples; ++sample) differs = differs || bits(pixel * Samples + sample) != sampleZero; + for (std::uint32_t read = 0; read < 3u; ++read) { + std::array texel{}; + std::copy_n(&Buffer[tid * Words + LoadResult + read * 4u], texel.size(), texel.begin()); + char where[200]; + std::snprintf(where, sizeof(where), "%s of pixel (%u, %u) read %08x %08x %08x %08x, sample 0 is %08x %08x %08x %08x", read == 0u ? "a DIM 2D image_load" : read == 1u ? "a DIM 2D image_sample_lz" : "a DIM 2D image_sample_l at LOD 1", pixel % Side, pixel / Side, texel[0], texel[1], texel[2], texel[3], sampleZero[0], sampleZero[1], sampleZero[2], sampleZero[3]); + Require(texel == sampleZero, std::string(where) + " " + what); + } + } + } + Require(!distinct || differs, "no pixel's sample 0 differs from its other samples " + what); +} + std::array CompressedDescriptor(const Block& block, std::uint32_t tileMode, std::uint64_t htile) { auto texture = TargetDescriptor(block, SampleShift, Format16161616Float, tileMode); const auto meta = htile >> 8u; @@ -885,12 +965,46 @@ int main(int argc, char** argv) { Block other(BlockBytes); const auto color = DecodeTarget(block); const auto state = TargetState(color); + Draw(*device, state); + const auto drawn = LoadSamples(*device, block); + CheckSamples(state, drawn, {}); + CheckFirstSampleReads(*device, TargetDescriptor(block, SampleShift), drawn, true, "from the drawn 4x target"); + Draw(*device, state, 0, 2u, Unorm8PixelCode); + const auto redrawn = LoadSamples(*device, block); + Require(redrawn != drawn, "drawing the 4x target again with another color left its samples unchanged"); + CheckFirstSampleReads(*device, TargetDescriptor(block, SampleShift), redrawn, true, "from the 4x target drawn again with another color after a first read"); + Block zeroed(BlockBytes); + CheckFirstSampleReads(*device, TargetDescriptor(zeroed, SampleShift), std::vector>(Side * Side * Samples), false, "from zeroed color memory no draw rendered to"); + Block aliased(4u * BlockBytes); + Draw(*device, TargetState(DecodeSwappedTarget(aliased)), 0, 2u, Unorm8PixelCode); + Draw(*device, TargetState(DecodeTarget(aliased))); + const auto aliasedSamples = LoadSamples(*device, aliased); + CheckSamples(state, aliasedSamples, {}); + CheckFirstSampleReads(*device, TargetDescriptor(aliased, SampleShift), aliasedSamples, true, "from a 4x RGBA16F target drawn after a BGRA8 target at the same address"); + std::puts("DIM 2D image_load, image_sample_lz and image_sample_l reads of a 4x color target return each pixel's sample 0"); + Block swapped(4u * BlockBytes); + Draw(*device, TargetState(DecodeSwappedTarget(swapped)), 0, 2u, Unorm8PixelCode); + const auto swappedSamples = LoadSamples(*device, swapped, Format8888Unorm); + CheckSwappedSamples(swappedSamples); + CheckFirstSampleReads(*device, TargetDescriptor(swapped, SampleShift, Format8888Unorm), swappedSamples, true, "from a 4x BGRA8 target read through an RGBA8 descriptor"); + std::puts("a 4x BGRA8 color target loaded through an RGBA8 texture descriptor reads its drawn color with red and blue swapped"); + Block integer(4u * BlockBytes); + Block signedFill(BlockBytes); + if ((AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).limits.sampledImageIntegerSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) { + std::puts("DIM 2D reads of 4x 8_8_8_8_UINT and 8_8_8_8_SINT targets not tested: sampledImageIntegerSampleCounts lacks VK_SAMPLE_COUNT_4_BIT"); + } else { + Draw(*device, TargetState(DecodeUintTarget(integer)), 0, 2u, Uint8PixelCode); + const auto integerSamples = LoadSamples(*device, integer, Format8888Uint); + CheckUintSamples(integerSamples); + CheckFirstSampleReads(*device, TargetDescriptor(integer, SampleShift, Format8888Uint), integerSamples, true, "from a drawn 4x 8_8_8_8_UINT target"); + StoreWords(signedFill, BlockBytes, FilledSint8); + CheckFirstSampleReads(*device, TargetDescriptor(signedFill, SampleShift, Format8888Sint), std::vector>(Side * Side * Samples, std::bit_cast>(FilledSint8Texel)), false, "from 8_8_8_8_SINT color memory no draw rendered to"); + std::puts("DIM 2D reads of a drawn 4x 8_8_8_8_UINT target and of 8_8_8_8_SINT color memory no draw rendered to return each pixel's sample 0 bit for bit"); + } if (!AgcDriver::VulkanDeviceTestAccess::ContextOf(*device).storageImageMultisample) { - std::puts("multisampled target sampling not tested: storage image multisampling unavailable"); + std::puts("multisampled target fills, stores and swizzle modes not tested: storage image multisampling unavailable"); return VulkanTestSkipped; } - Draw(*device, state); - CheckSamples(state, LoadSamples(*device, block), {}); const auto colorBytes = AgcDriver::Graphics::MultisampledColorBytes(color); Require(colorBytes != 0 && colorBytes <= block.bytes, "the 4x target's color memory does not fit its block"); AgcDriver::Graphics::NoteColorMetadataFill(block.Address(), static_cast(colorBytes), FillOnes); @@ -953,10 +1067,6 @@ int main(int argc, char** argv) { StoreWords(standard, colorBytes, FillOnes); RequireRefused(*device, TargetDescriptor(standard, SampleShift, Format16161616Float, TileS64KBX), "has no modeled multisampled color footprint", "loading an S swizzle multisampled texture no draw rendered to"); std::puts("every sample of a 4x color target loaded through its multisampled texture descriptor matches the triangle drawn into it, then the fill of its color memory; a surface no draw rendered to loads the value its color memory repeats and keeps it under a later draw"); - Block swapped(4u * BlockBytes); - Draw(*device, TargetState(DecodeSwappedTarget(swapped)), 0, 2u, Unorm8PixelCode); - CheckSwappedSamples(LoadSamples(*device, swapped, Format8888Unorm)); - std::puts("a 4x BGRA8 color target loaded through an RGBA8 texture descriptor reads its drawn color with red and blue swapped"); Block destination(BlockBytes); struct ReleaseIndexDevice { std::unique_ptr& device; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 45b3a7ea1e..10c4dd5100 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -128,6 +128,10 @@ std::uint32_t imageCoordinates(RdnaImageDimension dimension) { } } +bool multisamplePlaneRead(const ImageResource& image) { + return image.dimension == RdnaImageDimension::Dim2D && image.resourceClass == ImageResourceClass::Sampled && image.table == NoTable && !image.depthCompare && !image.r128 && image.cubeFaceCompatible; +} + bool validImageDescriptor(const DescriptorValue& descriptor, bool r128) { const auto type = rawImageType(descriptor); const auto format = rawImageFormat(descriptor); @@ -214,6 +218,7 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag } decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); + if (decoded.dimension == RdnaImageDimension::Dim2DMsaa && rawImageType(descriptor) == ImageType::Color2DMsaa && multisamplePlaneRead(base) && !IsFmaskTextureFormat(format) && !IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) decoded.dimension = RdnaImageDimension::Dim2D; if (base.atomic64 && format != IrBufferFormat::Format32_32UInt && format != IrBufferFormat::Format32_32SInt && format != IrBufferFormat::Format32_32Float) { return rejectImage(PoisonReason::InvalidFormat, "64-bit atomic image descriptor uses an unsupported format " + std::to_string(static_cast(format))); } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index d795f5d53f..81d74bddf9 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -163,6 +163,7 @@ - [Multisampled CMASK state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target has no guest texels, so its CMASK state comes from fills rather than from a read at each draw as a single-sample target's does. A fill of the whole CMASK with 0 clears the host image at the target's next use, one with 0xffffffff marks it expanded, and any other pattern, or a fill that covers only part of the CMASK, throws at the next use (inferred: main's rule that a mixed CMASK is unmodeled, applied to the one host image). A FAST_CLEAR target starts from its CMASK keys at its first use, and a COMPRESSION-only target starts expanded. Copies by DMA into the CMASK are not seen, and a later use with another CMASK address throws. The clear does not rewrite the CMASK bytes as expanded, so a new target over the same CMASK (a different extent, so a different image) seeds from them again and clears once more. - [Multisampled color fills](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - a multisampled target's samples are on the host, so a fill of its color memory is applied at its next use: a uniform fill over the whole of it clears the host image to that value, and a pending CMASK fast clear takes precedence over the fill and over a fill that would throw. Its color memory is a single mip and slice of addrlib's block dimensions for its sample count (ComputeThinBlockDimension). A fill over part of it, a fill reaching the start of a target whose footprint is not computed (linear), and a fill value with no exact clear value in the format (sRGB channels other than 0 and 1) throw at the next use. A fill before the target's first use is not seen (the host image starts at 0), nor are copies or shader writes into its color memory. A uniform compute fill over exactly the whole color memory with an exact clear value is not stored into guest memory, since only the host image holds the samples. A target is retired only when its whole guarded range is unmapped or its allocation removed, so if a title reuses that memory for other data while it stays mapped, a read the guard does not check (a buffer read through a V# or a pointer) sees the bytes from before the fill. - [Multisampled texture no draw rendered to](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - sampling a multisampled texture whose surface no draw rendered to creates the host target from the T# and clears it to the color a single 32-bit value repeated over the whole footprint encodes, padding up to whole tile blocks included. Inferred, not measured on the console: that the samples of such a surface are the guest color memory, as a title that clears its canvas on the CPU (ps5-opengl `ps5_clear_msaa4_color`) relies on. Memory that is not one repeated value, tile modes other than Standard4KB, Standard64KB and RenderTarget, unmapped memory and values with no exact clear color throw. CPU writes to the surface after its host target exists are not seen by the host. +- [Sample 0 of a multisampled color target](../../../core/libs/prx/libSceAgcDriver/Graphics/src/FirstSampleCopy.cpp) (libSceAgcDriver) - a 2D image read of a multisampled color target returns its float, UNORM, SNORM or sRGB sample 0 exactly only where the driver keeps those values through a shader and a color attachment, which Vulkan does not require, and one of a multisampled depth surface throws. The read binds a one-sample copy that a fragment pass (`texelFetch` of sample 0) draws before every such binding; integer formats copy exactly. - [Multisampled targets in CB passes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a CB eliminate or DCC pass over a multisampled target throws, since its texels are on the host and only a CB resolve and an FMASK decompress (`CB_COLOR_CONTROL` mode 5) are modeled. The FMASK decompress only applies the pending CMASK fast clear to the host image; it requires a CMASK address, and the target's DCC and FMASK contents are not interpreted. DCC over a multisampled target is accepted only in that pass and throws everywhere else. A CB resolve into a fast-clear destination throws: the resolve writes the destination's texels but not its CMASK, which would still read as cleared. - [Multisampled target guest access](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) (libSceAgcDriver) - the memory a multisampled color target throws on (CPU read, index buffer read, fixed-function vertex buffer read, DMA copy, texture or storage image read) uses fragment-aware 64 KiB render-target blocks. [AMD Addrlib's GFX10 thin-block rule](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L1680-L1712) gives the block width and height from element bytes and fragment count; its [surface-size calculation](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp#L4000-L4005) rounds both extents to those blocks and multiplies by element bytes and fragments. Thus a 1920x1080 4x RGBA8 surface occupies 0x1fe0000 bytes, leaving the next allocation at +0x2000000 outside the guard. The 2x and 8x block orientations differ from treating the fragments as a larger element. Console address-layout equivalence has not been measured. Other tile modes retain the conservative `max(CB bytes * samples, width * height * element bytes * samples)` bound, which may still reject a distinct allocation. Live targets remain cached per device, surface address, format, extent and sample count. Retired cache owners are removed on the next target, sampled or storage lookup; outstanding recorded work and sampled views retain their images. Ordinary sampled and storage bindings check the color-target guard before resource reuse, including precollected sampled lookups. Reused image tables do not invalidate on multisample color-target registration yet; their per-element fault handling and native multisample sampling require a separate reuse proof. - [DB depth copies](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - a draw with `DB_RENDER_CONTROL.DEPTH_COPY` is run as a compute pass that copies one sample of the `Z_32_FLOAT` depth surface into a single-sample R32 float color target over the whole surface. With `COPY_CENTROID` the sample is the first one the draw covers, which is sample 0 under the full `PA_SC_AA_MASK` the driver requires: [radeonsi copies one sample per draw with `COPY_CENTROID` and a one-sample mask](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-24.0.0/src/gallium/drivers/radeonsi/si_state.c#L1679-1681); the console is not measured. Stencil copies, depth tests, other depth formats, multisampled or other-format targets and copies over part of the surface throw. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 2b6ace99db..69f49a712c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -47,6 +47,7 @@ - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. The same assumption lets the two `s_load`s of one T# record match when one offset is added before `v_readfirstlane` and the other after (`ReadFirstLane(Select(mask, (key << 5) + 16, old), mask)` against `ReadFirstLane(key, mask) << 5`): with no active lane the hardware reads lane 0 of the inactive arm, and the table still binds the whole record. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. - Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read uncompressed `Fmask32_S8_F8` through a single-mip 2D or 2D-array `R32_UINT` snapshot with `Z_64KB_X` addressing. Destination selectors may name X, zero or one. Other FMASK formats, CMASK compression, packed accesses and other layouts throw. The layout follows PAL's [FMASK-to-surface conversion](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877) and [32-bit view selectors](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Device.cpp#L4973). Native RDNA/PS5 image-load results and title rendering are unmeasured; compressed color sampling remains unsupported. +- A [DIM 2D read of a 2D MSAA T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) returns sample 0, which is not measured on the console's gfx1013, at an implicit LOD above 0, at an explicit one for texels over 16 bits except formats 22 and 56 in SW_64KB_R_X, or for other formats and swizzle modes; 64- and 128-bit SW_64KB_Z_X texels are not covered: gfx1031 does not address their samples. Such a read is an `image_load` without a mip, `image_sample`, `image_sample_lz` or `image_sample_l` of a direct sampled image; other accesses (gathers, bias, gradients, offsets, LOD clamps, depth compares, `image_load_mip`, `image_get_resinfo`, `image_get_lod`, PCK, BY2/BY4) throw, as does a depth-bits T# (16_UINT, 32_UINT or 32_SINT in a Z swizzle mode). On gfx1031 (RX 6700 XT, HIP), such reads return sample 0 at 2x, 4x and 8x for 8- to 128-bit texels in SW_64KB_R_X and 8- to 32-bit ones in SW_64KB_Z_X (formats 1, 5, 7, 11, 13, 14, 18, 22, 29, 56, 60, 64, 71, 77, 133). - `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. - Uint and Sint [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp) entries and direct T#s under a table S# sample through a point-filtered copy of the S#: Vulkan leaves linear filtering of integer formats undefined; the console is assumed to point-sample. From b13deef0db01704fd485bbf6e261f18641b2dbbd Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:19:46 +0200 Subject: [PATCH 195/318] feat(libSceFreeType): implement FT_Get_Gasp --- core/libs/prx/libSceFreeType/Export.cpp | 7 +++ core/libs/tests/FreeTypeFontFixture.hpp | 15 ++++- core/libs/tests/GuestFreeType.cpp | 74 +++++++++++++++++++++++++ docs/dev/debt/libSceFreeType.md | 2 + 4 files changed, 95 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceFreeType/Export.cpp b/core/libs/prx/libSceFreeType/Export.cpp index 9eaae33cae..825bc9b1bb 100644 --- a/core/libs/prx/libSceFreeType/Export.cpp +++ b/core/libs/prx/libSceFreeType/Export.cpp @@ -3,6 +3,8 @@ #include "prx/libc/include/FilesystemError.hpp" #include "prx/libc/include/General.hpp" +#include FT_GASP_H + #include #include #include @@ -186,6 +188,11 @@ int APS5_VABI FT_Load_Glyph_nid_postfix(GuestFaceRec* face, std::uint32_t glyph_ return error; } +int APS5_VABI FT_Get_Gasp_nid_postfix(GuestFaceRec* face, std::uint32_t ppem) { + const GuestFace* owner = FaceOf(face); + return FT_Get_Gasp(owner ? owner->host : nullptr, ppem); +} + int APS5_VABI FT_Load_Char_nid_postfix(GuestFaceRec* face, std::uint64_t charCode, std::int32_t loadFlags) { GuestFace* owner = FaceOf(face); const FT_Error error = FT_Load_Char(owner ? owner->host : nullptr, static_cast(static_cast(charCode)), loadFlags); diff --git a/core/libs/tests/FreeTypeFontFixture.hpp b/core/libs/tests/FreeTypeFontFixture.hpp index 9458d09a81..b876a19139 100644 --- a/core/libs/tests/FreeTypeFontFixture.hpp +++ b/core/libs/tests/FreeTypeFontFixture.hpp @@ -20,7 +20,8 @@ static void PutAll16(std::vector& out, std::initializer_list for (const int value : values) Put16(out, static_cast(static_cast(value))); } -static std::vector SquareGlyphFont(std::uint32_t codepoint, int width, bool symbolMap = false) { +static std::vector SquareGlyphFont(std::uint32_t codepoint, int width, bool symbolMap = false, + const std::vector* gasp = nullptr) { std::vector head; Put32(head, 0x00010000u); Put32(head, 0x00010000u); @@ -53,11 +54,19 @@ static std::vector SquareGlyphFont(std::uint32_t codepoint, int w const int symbol = static_cast(codepoint + 0xF000); PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, symbol, 0xFFFF, 0, symbol, 0xFFFF, 1 - symbol, 1, 0, 0}); } - const std::vector*>> tables = { + std::vector*>> tables = { {"cmap", &cmap}, {"glyf", &glyf}, {"head", &head}, {"hhea", &hhea}, {"hmtx", &hmtx}, {"loca", &loca}, {"maxp", &maxp}}; + if (gasp) tables.insert(tables.begin() + 1, {"gasp", gasp}); + unsigned power = 1; + unsigned selector = 0; + while (power <= tables.size() / 2) { + power *= 2; + ++selector; + } std::vector font; Put32(font, 0x00010000u); - PutAll16(font, {static_cast(tables.size()), 64, 2, static_cast(tables.size()) * 16 - 64}); + PutAll16(font, {static_cast(tables.size()), static_cast(power * 16), static_cast(selector), + static_cast(tables.size() * 16 - power * 16)}); std::uint32_t offset = 12 + static_cast(tables.size()) * 16; for (const auto& [tag, data] : tables) { font.insert(font.end(), tag, tag + 4); diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index bccc9a51c2..a68e64258f 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -4,6 +4,7 @@ #include #include FT_FREETYPE_H +#include FT_GASP_H #include FT_OUTLINE_H #include @@ -28,6 +29,7 @@ extern "C" { void APS5_VABI FT_Set_Transform_nid_postfix(GuestFaceRec*, const GuestMatrix*, const GuestVector*); int APS5_VABI FT_Init_FreeType_nid_postfix(void**); int APS5_VABI FT_Done_FreeType_nid_postfix(void*); +int APS5_VABI FT_Get_Gasp_nid_postfix(GuestFaceRec*, std::uint32_t); int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFaceRec**); int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec*); @@ -47,6 +49,77 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +static void CheckGaspTable(const std::vector* table, const std::vector>& ranges, + bool versionZero, bool render) { + const auto font = SquareGlyphFont('A', 700, false, table); + void* library = nullptr; + GuestFaceRec* face = nullptr; + FT_Library nativeLibrary = nullptr; + FT_Face native = nullptr; + Require(FT_Init_FreeType_nid_postfix(&library) == 0 && FT_Init_FreeType(&nativeLibrary) == 0); + Require(FT_New_Memory_Face_nid_postfix(library, font.data(), font.size(), 0, &face) == 0); + Require(FT_New_Memory_Face(nativeLibrary, font.data(), static_cast(font.size()), 0, &native) == 0); + auto* slot = face->glyph; + const auto glyphIndex = slot->glyph_index; + const auto bitmap = slot->bitmap.buffer; + const auto points = slot->outline.points; + for (std::uint32_t ppem = 0; ppem <= 65536; ++ppem) { + int expected = FT_GASP_NO_TABLE; + for (const auto& [maximum, flags] : ranges) { + if (ppem > maximum) continue; + expected = versionZero ? flags & 3 : flags; + break; + } + Require(FT_Get_Gasp_nid_postfix(face, ppem) == expected); + Require(FT_Get_Gasp(native, ppem) == expected); + } + Require(FT_Get_Gasp_nid_postfix(face, UINT32_MAX) == FT_GASP_NO_TABLE); + Require(face->glyph == slot && slot->glyph_index == glyphIndex && slot->bitmap.buffer == bitmap && slot->outline.points == points); + if (render) { + for (const unsigned ppem : {7u, 8u, 15u, 16u, 31u, 32u, 64u}) { + const int flags = FT_Get_Gasp_nid_postfix(face, ppem); + const int load = flags & FT_GASP_DO_GRIDFIT ? FT_LOAD_DEFAULT : FT_LOAD_NO_HINTING; + const auto mode = flags & FT_GASP_DO_GRAY ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO; + Require(FT_Set_Pixel_Sizes_nid_postfix(face, 0, ppem) == 0 && FT_Set_Pixel_Sizes(native, 0, ppem) == 0); + Require(FT_Load_Char_nid_postfix(face, 'A', load) == 0 && FT_Load_Char(native, 'A', load) == 0); + Require(FT_Render_Glyph_nid_postfix(face->glyph, mode) == 0 && FT_Render_Glyph(native->glyph, mode) == 0); + const auto& actual = face->glyph->bitmap; + const auto& expected = native->glyph->bitmap; + Require(actual.width > 0 && actual.rows > 0); + Require(actual.width == expected.width && actual.rows == expected.rows && actual.pitch == expected.pitch); + Require(actual.pixel_mode == expected.pixel_mode && actual.num_grays == expected.num_grays); + Require(face->glyph->bitmap_left == native->glyph->bitmap_left && face->glyph->bitmap_top == native->glyph->bitmap_top); + Require(std::memcmp(actual.buffer, expected.buffer, static_cast(actual.rows) * std::abs(actual.pitch)) == 0); + } + } + Require(FT_Done_Face_nid_postfix(face) == 0 && FT_Done_Face(native) == 0); + Require(FT_Done_FreeType_nid_postfix(library) == 0 && FT_Done_FreeType(nativeLibrary) == 0); +} + +static void CheckGasp() { + Require(FT_Get_Gasp_nid_postfix(nullptr, 0) == FT_GASP_NO_TABLE); + Require(FT_Get_Gasp_nid_postfix(nullptr, UINT32_MAX) == FT_GASP_NO_TABLE); + CheckGaspTable(nullptr, {}, false, false); + const std::vector> ranges{{0, 0x8003}, {7, 2}, {15, 1}, {31, 0}, {65535, 0xFFFF}}; + for (const unsigned version : {0u, 1u}) { + std::vector table; + Put16(table, version); + Put16(table, ranges.size()); + for (const auto& [maximum, flags] : ranges) { + Put16(table, maximum); + Put16(table, flags); + } + CheckGaspTable(&table, ranges, version == 0, true); + } + std::vector shortRange; + PutAll16(shortRange, {1, 1, 48, FT_GASP_DO_GRAY}); + CheckGaspTable(&shortRange, {{48, FT_GASP_DO_GRAY}}, false, false); + for (const std::vector& table : {std::vector{}, {0, 1}, {0, 1, 0, 0}, {0, 2, 0, 1, 0, 10, 0, 3}}) { + CheckGaspTable(&table, {}, false, false); + } + std::puts("FreeType gasp: 8 fonts, 524304 queries, null faces and 14 rendered glyphs passed"); +} + template static bool ThrowsWith(TCall call, const std::string& prefix) { try { call(); @@ -361,6 +434,7 @@ static void CheckTransforms(const std::vector& font) { int main() { const auto root = std::filesystem::temp_directory_path() / ("anyps5_guest_freetype-" + std::to_string(std::random_device{}())); std::filesystem::create_directories(root / "app0"); + CheckGasp(); const std::vector font = SquareGlyphFont('A', 700); { std::ofstream file(root / "app0" / "font.ttf", std::ios::binary); diff --git a/docs/dev/debt/libSceFreeType.md b/docs/dev/debt/libSceFreeType.md index d56614d548..fc27092fbb 100644 --- a/docs/dev/debt/libSceFreeType.md +++ b/docs/dev/debt/libSceFreeType.md @@ -22,3 +22,5 @@ - [FT_Outline_Get_Orientation](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI/version and large-outline equivalence are unverified. The adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftoutln.c) with the existing unsigned guest counts and an LP64 area accumulator, including on Windows. - [FT_Set_Char_Size](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console ABI, FreeType version and error compatibility are unverified. The name and NID `xuY99Qcb108` are in the [PS5 3.20 inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceFreeTypeOt.c); the LP64 signature and behavior follow bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftobjs.c), not a console measurement. After applying its defaults and minimum-size clamp, requests whose `dimension * dpi + 36` exceeds the host's signed `FT_Long` range throw before the native call. This is a 32-bit limit on Windows; larger console requests remain unsupported. + +- [FT_Get_Gasp](../../../core/libs/prx/libSceFreeType/Export.cpp) (libSceFreeType) - console FreeType version and query-result compatibility are unverified. The guest face adapter follows bundled [FreeType 2.13.3](https://github.com/freetype/freetype/blob/42608f77f20749dd6ddc9e0536788eaad70ea4b5/src/base/ftgasp.c) and its existing LP64 face bridge. From ac06b871a9299345e55f64f7fa6e632bf02262ef Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:22:31 +0200 Subject: [PATCH 196/318] test(libSceFreeType): isolate the gasp query declaration --- core/libs/tests/GuestFreeType.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestFreeType.cpp b/core/libs/tests/GuestFreeType.cpp index a68e64258f..ff611d19b4 100644 --- a/core/libs/tests/GuestFreeType.cpp +++ b/core/libs/tests/GuestFreeType.cpp @@ -29,7 +29,6 @@ extern "C" { void APS5_VABI FT_Set_Transform_nid_postfix(GuestFaceRec*, const GuestMatrix*, const GuestVector*); int APS5_VABI FT_Init_FreeType_nid_postfix(void**); int APS5_VABI FT_Done_FreeType_nid_postfix(void*); -int APS5_VABI FT_Get_Gasp_nid_postfix(GuestFaceRec*, std::uint32_t); int APS5_VABI FT_New_Face_nid_postfix(void*, const char*, std::int64_t, GuestFaceRec**); int APS5_VABI FT_New_Memory_Face_nid_postfix(void*, const unsigned char*, std::int64_t, std::int64_t, GuestFaceRec**); int APS5_VABI FT_Done_Face_nid_postfix(GuestFaceRec*); @@ -49,6 +48,8 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +extern "C" int APS5_VABI FT_Get_Gasp_nid_postfix(GuestFaceRec*, std::uint32_t); + static void CheckGaspTable(const std::vector* table, const std::vector>& ranges, bool versionZero, bool render) { const auto font = SquareGlyphFont('A', 700, false, table); From df421d52bcf64605f9a99c003de55deb0975a130 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Tue, 20 Oct 2026 07:28:18 -0400 Subject: [PATCH 197/318] fix(agc): verify refreshed draw stage data --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +++++ .../Execution/include/Driver/Driver.hpp | 2 +- .../Execution/src/Driver/Draw/Draw.cpp | 2 +- .../Execution/src/Driver/Draw/DrawCapture.cpp | 22 +++++++++------- .../tests/execution/DrawCacheReuse.cpp | 26 ++++++++++++++++--- 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 48689954f3..452bbae9b6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2780,6 +2780,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_cache_reuse_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_GPL=1" SKIP_RETURN_CODE 77) add_test(NAME agc_driver_draw_cache_reuse_verify COMMAND agc_driver_draw_cache_reuse_tests) set_tests_properties(agc_driver_draw_cache_reuse_verify PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_driver_draw_cache_reuse_entry_verify COMMAND agc_driver_draw_cache_reuse_tests) + set_tests_properties(agc_driver_draw_cache_reuse_entry_verify PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_VERIFY_DRAW_ENTRIES=1" SKIP_RETURN_CODE 77 FAIL_REGULAR_EXPRESSION "FATAL:") + if(WIN32) + set_tests_properties(agc_driver_draw_cache_reuse_entry_verify PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_draw_cache_reuse_inline COMMAND agc_driver_draw_cache_reuse_tests) set_tests_properties(agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0;APS5_VERIFY_DRAW_RECIPE=1;APS5_VERIFY_REGISTER_KEYS=1" SKIP_RETURN_CODE 77) set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:") diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 1bba99967e..1d22afe65a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -173,7 +173,7 @@ private: void lookupDraw(const Submission& submission, const std::shared_ptr& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector& programs, const std::vector& roles, const std::vector>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr& entry, std::vector>& matched, std::vector>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array& phaseMs, const std::shared_ptr& dataEntry, DrawDataCandidates& dataCandidates, std::vector& stageData); std::shared_ptr materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector>& vertexInfos, std::vector& memory, const std::vector& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr& localDevice, ShaderMemory& shaderMemory, std::vector& stageCaptures, std::vector& recompiled, bool drawHit, const std::vector>& matched, const std::vector>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array& phaseMs, std::string& rejected, const std::optional& meshBarycentrics); static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector& decodeReads); - void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming, const std::vector& reused, const std::shared_ptr& entry, std::uint64_t shapeKey, bool shapeNeedsEntry = false); + void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, const std::vector& stageData, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming, const std::vector& reused, const std::shared_ptr& entry, std::uint64_t shapeKey, bool shapeNeedsEntry = false); static bool drawPrecheck(); std::optional precheckDraw(const QueueState& queue, const Submission& submission, std::span packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect); static std::uint32_t drawUserWord(const DrawProgram& program, std::int32_t sgpr); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp index 28c9800bb3..5527b71fec 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -796,7 +796,7 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) { return lastData[shapeKey % lastData.size()].exchange(data, std::memory_order_relaxed) == data; }; const bool shapeNeedsEntry = shapeRefresh && registerKey && shapeKey != 0 && drawDataHits() && !drawParameters.indirect && dataEntry != nullptr && dataStagesCompiled != 0 && refreshSeenTwice(); - cacheDrawStages(stageEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, fresh, drawKey, registerKey, decode, phaseTiming, reused, entry, shapeKey, shapeNeedsEntry); + cacheDrawStages(stageEntries, drawHit, drawParameters, indirectCpu, programs, stageCaptures, vertexInfos, decodeReads, verifyHit, matched, stageData, fresh, drawKey, registerKey, decode, phaseTiming, reused, entry, shapeKey, shapeNeedsEntry); timing.Mark("shader_compile_and_link"); for (const auto& reads : decodeReads) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp index a76fd8e08d..288ca494b9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp @@ -152,7 +152,7 @@ void Driver::recordStageDataPositions(DispatchVariant& variant, const ShaderReco variant.dataSlots.resize(kept); } -void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming, const std::vector& reused, const std::shared_ptr& entry, std::uint64_t shapeKey, bool shapeNeedsEntry) { +void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional& indirectCpu, const std::vector& programs, const std::vector& stageCaptures, const std::vector>& vertexInfos, const std::vector>& decodeReads, bool verifyHit, const std::vector>& matched, const std::vector& stageData, std::vector>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr& decode, DrawPhaseTiming& phaseTiming, const std::vector& reused, const std::shared_ptr& entry, std::uint64_t shapeKey, bool shapeNeedsEntry) { const bool cacheable = useDrawEntries && !drawHit && !(drawParameters.indirect && indirectCpu); const bool admitted = cacheable && (verifyHit || admitDrawKey(drawKey, registerKey && drawDataHits() ? shapeKey : 0)); const bool replaceShape = cacheable && !admitted && shapeNeedsEntry; @@ -192,17 +192,17 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP ++differingRunsChanged; } } - const auto sameContent = [](const DispatchVariant& a, const DispatchVariant& b) { - const auto flatten = [](const DispatchVariant& source) { + const auto sameContent = [](const DispatchVariant& a, const std::vector& words, const DispatchVariant& b) { + const auto flatten = [](const DispatchVariant& source, const std::vector& values) { std::vector> flat; std::size_t word = 0; for (const auto& [begin, end] : source.runs) { - for (auto address = begin; address < end && word < source.words.size(); address += sizeof(std::uint32_t)) flat.emplace_back(address, source.words[word++]); + for (auto address = begin; address < end && word < values.size(); address += sizeof(std::uint32_t)) flat.emplace_back(address, values[word++]); } std::sort(flat.begin(), flat.end()); return flat; }; - return (a.runs == b.runs && a.words == b.words) || flatten(a) == flatten(b); + return (a.runs == b.runs && words == b.words) || flatten(a, words) == flatten(b, b.words); }; const auto sameForwardedWords = [](const ShaderRecompiler::RecompileResult& a, const ShaderRecompiler::RecompileResult& b, std::uint32_t hidden) { if (a.pushConstants.size() != b.pushConstants.size() || a.shaderDataUserWords != b.shaderDataUserWords) return false; @@ -211,10 +211,14 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP } return true; }; - if (verifyHit && matched[i] != nullptr && (!sameContent(*matched[i], *variant) || !sameForwardedWords(*matched[i]->compiled, *variant->compiled, programs[i].hiddenUserWords))) { - ++mismatches; - static std::atomic reports{0}; - if (reports.fetch_add(1) < 20) std::fprintf(stderr, "[draw-cache] verify: stage %zu (program 0x%llx) of a hit captured differently: %zu runs / %zu words matched, %zu / %zu fresh\n", i, static_cast(programs[i].binary.codeAddress), matched[i]->runs.size(), matched[i]->words.size(), variant->runs.size(), variant->words.size()); + if (verifyHit && matched[i] != nullptr) { + const auto& words = stageData[i].compiled != nullptr ? stageData[i].words : matched[i]->words; + const auto& compiled = stageData[i].compiled != nullptr ? *stageData[i].compiled : *matched[i]->compiled; + if (!sameContent(*matched[i], words, *variant) || !sameForwardedWords(compiled, *variant->compiled, programs[i].hiddenUserWords)) { + ++mismatches; + static std::atomic reports{0}; + if (reports.fetch_add(1) < 20) std::fprintf(stderr, "[draw-cache] verify: stage %zu (program 0x%llx) of a hit captured differently: %zu runs / %zu words matched, %zu / %zu fresh\n", i, static_cast(programs[i].binary.codeAddress), matched[i]->runs.size(), words.size(), variant->runs.size(), variant->words.size()); + } } if (insertCompare()) { const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::DrawCache); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp index ab9818c6ba..41ff2d9a50 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawCacheReuse.cpp @@ -183,6 +183,20 @@ void StageDataHitsRefreshTheirWords(bool libraries) { Require(BrightestColorByte() == 255, mode + ": the draw of constant 1.0 wrote " + std::to_string(BrightestColorByte()) + ", expected 255"); } +void RefreshedStageDataVerifies() { + Registered(TriangleVertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2); + Registered(ConstantPixelHeader, ConstantPixelCode.data(), sizeof(ConstantPixelCode), 1); + ColorMemory.fill(std::byte{0}); + PixelConstant[0] = 0.25f; + const auto cached = SubmitConstantDraws(3); + Require(BrightestColorByte() == 64, "draw-entry verification: the initial constant did not render"); + PixelConstant[0] = 1.0f; + const auto refreshed = SubmitConstantDraws(1); + Require(refreshed.verifyHits - cached.verifyHits == 1 && refreshed.stageDataHits - cached.stageDataHits == 1, "draw-entry verification: the refreshed constant did not exercise a verified stage data hit"); + Require(refreshed.verifyMismatches == cached.verifyMismatches, "draw-entry verification: refreshed stage data differed from a fresh capture"); + Require(BrightestColorByte() == 255, "draw-entry verification: the refreshed constant did not render"); +} + void RetainedPixelCacheTransitions() { auto commands = Draws(32.0f, VertexCode.data(), 1, PixelCode.data(), 1, {}); Append(commands, 0x76, {0x00c, std::bit_cast(1.0f)}); @@ -239,10 +253,14 @@ int main() { libraries = device->GraphicsPipelineLibraries(); } if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped; - ThirdDrawHitsTheCache(libraries); - UserDataDrawsReuseTheirShape(libraries); - StageDataHitsRefreshTheirWords(libraries); - RetainedPixelCacheTransitions(); + if (std::getenv("APS5_VERIFY_DRAW_ENTRIES") != nullptr) { + RefreshedStageDataVerifies(); + } else { + ThirdDrawHitsTheCache(libraries); + UserDataDrawsReuseTheirShape(libraries); + StageDataHitsRefreshTheirWords(libraries); + RetainedPixelCacheTransitions(); + } AgcDriverShutdown_nid_postfix(); Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 0, "the driver's shutdown left the pipeline library worker running"); std::cout << "A repeated draw reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n"; From 46ff145113d75bfb5c179287fbc8cd3b85fde550 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Wed, 21 Oct 2026 21:52:49 +0600 Subject: [PATCH 198/318] fix(tools): flag qualified imports exported only under a NID --- docs/user/USAGE.md | 2 ++ tools/import_audit.py | 10 +++++-- tools/tests/test_import_audit.py | 51 ++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 988659c9ae..6cd3e2ad5f 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -387,6 +387,8 @@ python3 tools/import_audit.py app.registry.json app.libc.prx.guest.prx.registry. | `absent` | No built library exports the NID: the loader fails. | | `module` | The import names a file found in `--modules` (the title's own library), which is trusted to provide it. | +An import that names a library requires the literal export name. If its implementation is found only by hashing the import name, it still counts as `implemented` or `stub`, but the report lists the name mismatch, sets `symbol_mismatch` on the JSON import record and counts it in the summary, and exits with code 1. A strong import with a name mismatch is a current stop. Imports without a library retain the audit's name-to-NID fallback. + Module registries mark ELF weak imports with `"weak": true`, allowing the audit's current-stop selection to skip them. Strong imports omit the field. Older module registries do not preserve weak bindings; regenerate them to distinguish optional imports. ELF modules in `--modules` are audited regardless of filename: their undefined symbols are classified against the built libraries and the other modules' exports, with the module file named in the report, and the libraries they need must have a file in `--libs` or `--modules`. Ordinary non-module sidecars are ignored; malformed ELF, PE/SELF containers and non-ELF files with module extensions remain errors. The audit reads dynamic tables from ELF section headers or, for sectionless files, from `PT_DYNAMIC` and a SysV or GNU hash table. diff --git a/tools/import_audit.py b/tools/import_audit.py index 6ee58ad673..2f445230b9 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -594,6 +594,7 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu record["providers"] = providers record["library_mismatch"] = kind in ("implemented", "stub") and bool(library) and library not in providers and "libc.prx" not in providers record["name"] = next((stubs[(provider, resolved_nid)] for provider in providers if (provider, resolved_nid) in stubs), None) if kind == "stub" else None + record["symbol_mismatch"] = kind in ("implemented", "stub") and resolved_nid != nid and bool(library) ordered = sorted(records.values(), key=lambda record: (record["class"], record["library"] or "", record.get("module") or "", record["nid"])) check_conservation(len(imports) + sum(len(symbols) for symbols, _ in (module_imports or {}).values()), ordered) missing = defaultdict(int) @@ -618,7 +619,7 @@ def current_stop(imports, records): if weak: continue record = classes.get((nid, library)) - if record is not None and record["class"] == "absent": + if record is not None and (record["class"] == "absent" or record["symbol_mismatch"]): return record return None @@ -677,6 +678,7 @@ def summarize(records, references, missing, missing_hosts=()): "missing_libraries": missing, "missing_host_dependencies": [{"importer": entry["importer"], "chain": list(entry["chain"])} for entry in missing_hosts], "library_mismatch": sum(1 for record in records if record["library_mismatch"]), + "symbol_mismatch": sum(1 for record in records if record["symbol_mismatch"]), } @@ -722,6 +724,10 @@ def render(summary, records, missing, stops, missing_hosts=(), located=None): current = record["library"] lines.append(f" [{current or 'library not named'}]") lines.append(f" {label(record)}") + mismatched = [record for record in records if record["symbol_mismatch"]] + if mismatched: + lines.extend(["", f"Qualified import names exported only as their NID ({len(mismatched)}): the loader cannot resolve these names"]) + lines.extend(f" {label(record)} -> {nid_names.compute_nid(record['nid'])}" for record in mismatched) if stops: lines.append("") lines.append("Current stop (first unresolved import, in registry order)") @@ -800,7 +806,7 @@ def main(argv=None): return 2 located = locate_missing(missing, {path.resolve().parent for path in args.registries}) if missing else None print(render(summary, records, missing, stops, missing_hosts, located)) - return 1 if summary["unique_by_class"]["absent"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) or (args.strict_stubs and summary["unique_by_class"]["stub"]) else 0 + return 1 if summary["unique_by_class"]["absent"] or summary["symbol_mismatch"] or missing or missing_hosts or (args.strict_providers and summary["library_mismatch"]) or (args.strict_stubs and summary["unique_by_class"]["stub"]) else 0 if __name__ == "__main__": diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index a29775c13b..8d5d3844b3 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -290,6 +290,57 @@ class ImportAuditTests(unittest.TestCase): self.assertTrue(records["missing_function"]["weak"]) self.assertIsNone(data["stops"][0]["nid"]) + def test_nid_only_exports_do_not_resolve_literal_import_names(self): + work = self.work + name, nid = "strlen", nid_names.compute_nid("strlen") + registry, report = work.root / "sdk.registry.json", work.root / "audit.json" + for image in (exporting(nid), build_pe([nid])): + (work.libs / "libc.prx").write_bytes(image) + for stub in (False, True): + write_source(work.source, "libc", (STUB_SOURCE if stub else DONE_SOURCE).format(name=name)) + for weak in (False, True): + with self.subTest(pe=image.startswith(b"MZ"), stub=stub, weak=weak): + write_registry(registry, [(name, "libc.prx", weak)]) + code, out, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (1, "")) + data = json.loads(report.read_text(encoding="utf-8")) + record, = data["imports"] + self.assertEqual(record["class"], "stub" if stub else "implemented") + self.assertEqual(record["providers"], ["libc.prx"]) + self.assertTrue(record["symbol_mismatch"]) + self.assertEqual(data["symbol_mismatch"], 1) + self.assertEqual(data["stops"][0]["nid"], None if weak else name) + self.assertIn("Qualified import names exported only as their NID", out) + self.assertIn(f"-> {nid}", out) + + def test_literal_exports_resolve_without_a_name_mismatch(self): + work = self.work + name, nid = "strlen", nid_names.compute_nid("strlen") + registry, report = work.root / "sdk.registry.json", work.root / "audit.json" + for image in (exporting(name, nid), build_pe(sorted([name, nid]))): + with self.subTest(pe=image.startswith(b"MZ")): + (work.libs / "libc.prx").write_bytes(image) + write_registry(registry, [(name, "libc.prx"), (nid, "libc.prx")]) + code, out, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (0, "")) + data = json.loads(report.read_text(encoding="utf-8")) + self.assertEqual(data["symbol_mismatch"], 0) + self.assertTrue(all(not record["symbol_mismatch"] for record in data["imports"])) + self.assertIsNone(data["stops"][0]["nid"]) + self.assertNotIn("Qualified import names exported only as their NID", out) + + def test_unscoped_windows_imports_keep_the_loaders_nid_fallback(self): + work = self.work + name, nid = "strlen", nid_names.compute_nid("strlen") + registry, report = work.root / "sdk.registry.json", work.root / "audit.json" + write_registry(registry, [(name, "")]) + (work.libs / "libc.prx").write_bytes(build_pe([nid])) + code, out, err = work.run(registry, "--json", str(report)) + self.assertEqual((code, err), (0, "")) + data = json.loads(report.read_text(encoding="utf-8")) + self.assertEqual(data["symbol_mismatch"], 0) + self.assertIsNone(data["stops"][0]["nid"]) + def test_named_nid_fallback_preserves_stub_and_library_classification(self): nid = nid_names.compute_nid(MESSAGE_NAME) imports = [(MESSAGE_NAME, "libSceOther.prx", False)] From b7a6e9d245e190a0fc2ba1d26f7fc9a84c482b98 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Fri, 23 Oct 2026 12:23:51 +0600 Subject: [PATCH 199/318] fix(tools): decouple symbol mismatches from stub classification --- tools/import_audit.py | 3 ++- tools/tests/test_import_audit.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/import_audit.py b/tools/import_audit.py index 2f445230b9..a074a6105a 100644 --- a/tools/import_audit.py +++ b/tools/import_audit.py @@ -584,9 +584,11 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu if not library: providers = sorted(set(providers) | set((module_exports or {}).get(nid, []))) resolved_nid = nid + record["symbol_mismatch"] = False if not providers: resolved_nid = nid_names.compute_nid(nid) providers = exports.get(resolved_nid, []) + record["symbol_mismatch"] = bool(library) and library not in modules and bool(providers) and resolved_nid != nid if not library: providers = sorted(set(providers) | set((module_exports or {}).get(resolved_nid, []))) kind = classify(resolved_nid, library, providers, stubs, modules) @@ -594,7 +596,6 @@ def audit(imports, exports, stubs, libraries, modules, module_imports=None, modu record["providers"] = providers record["library_mismatch"] = kind in ("implemented", "stub") and bool(library) and library not in providers and "libc.prx" not in providers record["name"] = next((stubs[(provider, resolved_nid)] for provider in providers if (provider, resolved_nid) in stubs), None) if kind == "stub" else None - record["symbol_mismatch"] = kind in ("implemented", "stub") and resolved_nid != nid and bool(library) ordered = sorted(records.values(), key=lambda record: (record["class"], record["library"] or "", record.get("module") or "", record["nid"])) check_conservation(len(imports) + sum(len(symbols) for symbols, _ in (module_imports or {}).values()), ordered) missing = defaultdict(int) diff --git a/tools/tests/test_import_audit.py b/tools/tests/test_import_audit.py index 8d5d3844b3..b57fdf0b49 100644 --- a/tools/tests/test_import_audit.py +++ b/tools/tests/test_import_audit.py @@ -313,6 +313,17 @@ class ImportAuditTests(unittest.TestCase): self.assertIn("Qualified import names exported only as their NID", out) self.assertIn(f"-> {nid}", out) + def test_name_mismatch_requires_a_resolved_host_fallback(self): + name, library = "strlen", "libc.prx" + exports = {nid_names.compute_nid(name): [library]} + for providers, modules, kind, mismatch in (({}, set(), "absent", False), + (exports, {library}, "module", False), + (exports, set(), "implemented", True)): + with self.subTest(kind=kind): + records, _ = import_audit.audit([(name, library, False)], providers, {}, {library}, modules) + self.assertEqual(records[0]["class"], kind) + self.assertEqual(records[0]["symbol_mismatch"], mismatch) + def test_literal_exports_resolve_without_a_name_mismatch(self): work = self.work name, nid = "strlen", nid_names.compute_nid("strlen") From 705f6e1580202075f5a80fb6fe01193ba07b000b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 06:33:36 +0000 Subject: [PATCH 200/318] feat(libkernel_sys): implement chown, lchown, fchownat and linkat All four exports threw. The ownership calls follow fchown: owner and group 0 and the unchanged value are accepted and any other value throws. The path is looked up first with stat, lstat or fstatat, as FreeBSD 11's kern_fchownat does, so a lookup error is returned before the owner is checked. fchownat rejects flags other than AT_SYMLINK_NOFOLLOW with EINVAL. linkat passes AT_FDCWD and absolute paths to link and rejects flags other than AT_SYMLINK_FOLLOW with EINVAL; a relative path with a directory descriptor and a symbolic link source without AT_SYMLINK_FOLLOW throw. On Windows a symbolic link source is detected with the file attributes, since libstdc++ there reports it as a regular file. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../prx/libkernel_sys/Export.Discovery.cpp | 20 ----- core/libs/prx/libkernel_sys/Export.cpp | 51 ++++++++++++ core/libs/tests/GuestKernelSys.cpp | 81 +++++++++++++++++++ docs/dev/debt/libkernel_sys.md | 2 + 4 files changed, 134 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.Discovery.cpp b/core/libs/prx/libkernel_sys/Export.Discovery.cpp index e60ae139a5..e2a7678d17 100644 --- a/core/libs/prx/libkernel_sys/Export.Discovery.cpp +++ b/core/libs/prx/libkernel_sys/Export.Discovery.cpp @@ -92,11 +92,6 @@ int APS5_VABI blockpool_unmap_nid_postfix(void) { return 0; } -int APS5_VABI chown_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI cpuset_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -117,11 +112,6 @@ int APS5_VABI dynlib_get_obj_member_nid_postfix(void) { return 0; } -int APS5_VABI fchownat_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI futimesat_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -332,16 +322,6 @@ int APS5_VABI lchflags_nid_postfix(void) { return 0; } -int APS5_VABI lchown_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI linkat_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI mdbg_call_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 53ab5bc867..0ff29a6c8a 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -101,6 +101,7 @@ constexpr int PcAclPathMax = 60; constexpr int PcMacPresent = 63; constexpr int PcAclNfs4 = 64; constexpr int GuestAtSymlinkNofollow = 0x0200; +constexpr int GuestAtSymlinkFollow = 0x0400; int GuestFileError(int error) { switch (error) { @@ -152,6 +153,22 @@ bool HasNonDirectoryPrefix(const std::filesystem::path& native) { return false; } +void RequireGuestOwner(const char* call, std::uint32_t uid, std::uint32_t gid) { + constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; + if (uid != 0 && uid != Unchanged) NotImplemented_nid_no_patch((std::string(call) + ": an owner other than the guest user").c_str()); + if (gid != 0 && gid != Unchanged) NotImplemented_nid_no_patch((std::string(call) + ": a group other than the guest group").c_str()); +} + +bool IsSymbolicLink(const std::filesystem::path& native) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); + return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; +#else + std::error_code error; + return std::filesystem::is_symlink(std::filesystem::symlink_status(native, error)); +#endif +} + std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, std::size_t size) { #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); @@ -416,6 +433,17 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { return 0; } +int APS5_VABI linkat_nid_postfix(int fd1, const char* path1, int fd2, const char* path2, int flag) { + if (flag & ~GuestAtSymlinkFollow) return static_cast(Fail(GuestEinval)); + if (path1 == nullptr || path2 == nullptr) return static_cast(Fail(GuestEfault)); + if ((fd1 != GuestAtFdcwd && *path1 != '/') || (fd2 != GuestAtFdcwd && *path2 != '/')) + NotImplemented_nid_no_patch("linkat: a relative path with a directory descriptor"); + std::filesystem::path source; + if (flag == 0 && *path1 != '\0' && ResolvePathError_nid_no_patch(path1, source) == 0 && IsSymbolicLink(source)) + NotImplemented_nid_no_patch("linkat: a hard link to a symbolic link itself"); + return link_nid_postfix(path1, path2); +} + int APS5_VABI fchdir_nid_postfix(int fd) { int error = 0; const char* path = GetDirectoryDescriptorPath_nid_no_patch(fd, &error); @@ -627,6 +655,29 @@ int APS5_VABI fchown_nid_postfix(int fd, std::uint32_t uid, std::uint32_t gid) { int APS5_VABI stat_nid_postfix(const char* path, FileStat* sb); int APS5_VABI lstat_nid_postfix(const char* path, FileStat* sb); +int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag); + +int APS5_VABI chown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid) { + FileStat status{}; + if (stat_nid_postfix(path, &status) != 0) return -1; + RequireGuestOwner("chown", uid, gid); + return 0; +} + +int APS5_VABI lchown_nid_postfix(const char* path, std::uint32_t uid, std::uint32_t gid) { + FileStat status{}; + if (lstat_nid_postfix(path, &status) != 0) return -1; + RequireGuestOwner("lchown", uid, gid); + return 0; +} + +int APS5_VABI fchownat_nid_postfix(int fd, const char* path, std::uint32_t uid, std::uint32_t gid, int flag) { + if (flag & ~GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); + FileStat status{}; + if (fstatat_nid_postfix(fd, path, &status, flag) != 0) return -1; + RequireGuestOwner("fchownat", uid, gid); + return 0; +} int APS5_VABI fstatat_nid_postfix(int fd, const char* path, FileStat* sb, int flag) { if (flag & ~GuestAtSymlinkNofollow) return static_cast(Fail(GuestEinval)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index b481f508fe..e080ec6745 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -63,6 +63,10 @@ int APS5_VABI renameat_nid_postfix(int, const char*, int, const char*); int APS5_VABI fchmodat_nid_postfix(int, const char*, int, int); int APS5_VABI lchmod_nid_postfix(const char*, std::uint16_t); int APS5_VABI fchown_nid_postfix(int, std::uint32_t, std::uint32_t); +int APS5_VABI chown_nid_postfix(const char*, std::uint32_t, std::uint32_t); +int APS5_VABI lchown_nid_postfix(const char*, std::uint32_t, std::uint32_t); +int APS5_VABI fchownat_nid_postfix(int, const char*, std::uint32_t, std::uint32_t, int); +int APS5_VABI linkat_nid_postfix(int, const char*, int, const char*, int); int APS5_VABI fstatat_nid_postfix(int, const char*, FileStat*, int); std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); std::uint16_t APS5_VABI umask_nid_postfix(std::uint16_t); @@ -98,6 +102,82 @@ bool ChrootThrows(const std::string& path) { return false; } +template +bool ThrowsNaming(Call call, const char* text) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), text) != nullptr; + } + return false; +} + +void OwnershipAndLinkTests(const std::filesystem::path& root) { + constexpr std::uint32_t Unchanged = 0xFFFFFFFFu; + const auto file = (root / "owned.txt").string(); + std::ofstream(file).put('x'); + const auto missing = (root / "missing.txt").string(); + const auto throughFile = file + "/inner"; + const auto linked = (root / "linked.txt").string(); + const auto other = (root / "other.txt").string(); + Require(chown_nid_postfix(file.c_str(), 0, 0) == 0); + Require(chown_nid_postfix(file.c_str(), Unchanged, Unchanged) == 0); + Require(lchown_nid_postfix(file.c_str(), 0, Unchanged) == 0); + Require(fchownat_nid_postfix(-100, file.c_str(), Unchanged, 0, 0) == 0); + Require(fchownat_nid_postfix(-100, file.c_str(), 0, 0, 0x200) == 0); + for (const auto call : {chown_nid_postfix, lchown_nid_postfix}) { + Require(call(missing.c_str(), 0, 0) == -1 && Error() == 2); + Require(call(throughFile.c_str(), 0, 0) == -1 && Error() == 20); + Require(call("", 0, 0) == -1 && Error() == 2); + Require(call(nullptr, 0, 0) == -1 && Error() == 14); + } + Require(fchownat_nid_postfix(-100, missing.c_str(), 0, 0, 0) == -1 && Error() == 2); + Require(fchownat_nid_postfix(-100, file.c_str(), 0, 0, 0x400) == -1 && Error() == 22); + Require(chown_nid_postfix(missing.c_str(), 1000, 0) == -1 && Error() == 2); + Require(chown_nid_postfix(nullptr, 1000, 0) == -1 && Error() == 14); + Require(lchown_nid_postfix(throughFile.c_str(), 0, 1000) == -1 && Error() == 20); + Require(fchownat_nid_postfix(-100, missing.c_str(), 1000, 0, 0) == -1 && Error() == 2); + Require(fchownat_nid_postfix(4321, "owned.txt", 1000, 0, 0) == -1 && Error() == 9); + Require(fchownat_nid_postfix(-100, file.c_str(), 1000, 0, 0x400) == -1 && Error() == 22); + Require(ThrowsNaming([&] { chown_nid_postfix(file.c_str(), 1000, 0); }, "chown: an owner")); + Require(ThrowsNaming([&] { lchown_nid_postfix(file.c_str(), 0, 1000); }, "lchown: a group")); + Require(ThrowsNaming([&] { fchownat_nid_postfix(-100, file.c_str(), 5, Unchanged, 0); }, "fchownat: an owner")); + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + Require(fchownat_nid_postfix(directory, "owned.txt", 0, 0, 0) == 0); + Require(fchownat_nid_postfix(directory, "missing.txt", 0, 0, 0) == -1 && Error() == 2); + Require(linkat_nid_postfix(-100, file.c_str(), -100, linked.c_str(), 0) == 0); + Require(std::filesystem::hard_link_count(file) == 2); + Require(linkat_nid_postfix(-100, file.c_str(), -100, linked.c_str(), 0x400) == -1 && Error() == 17); + Require(linkat_nid_postfix(-100, missing.c_str(), -100, other.c_str(), 0x400) == -1 && Error() == 2); + Require(linkat_nid_postfix(-100, file.c_str(), -100, other.c_str(), 0x200) == -1 && Error() == 22); + Require(linkat_nid_postfix(-100, nullptr, -100, other.c_str(), 0) == -1 && Error() == 14); + Require(linkat_nid_postfix(-100, "", -100, other.c_str(), 0) == -1 && Error() == 2); + Require(ThrowsNaming([&] { linkat_nid_postfix(directory, "owned.txt", -100, other.c_str(), 0); }, "linkat: a relative path")); + Require(ThrowsNaming([&] { linkat_nid_postfix(-100, file.c_str(), directory, "other.txt", 0); }, "linkat: a relative path")); + const auto symbolic = (root / "owned.link").string(); + const auto dangling = (root / "dangling.link").string(); + if (symlink_nid_postfix("owned.txt", symbolic.c_str()) == 0) { + Require(symlink_nid_postfix("missing.txt", dangling.c_str()) == 0); + Require(chown_nid_postfix(symbolic.c_str(), 0, 0) == 0 && lchown_nid_postfix(symbolic.c_str(), 0, 0) == 0); + Require(chown_nid_postfix(dangling.c_str(), 0, 0) == -1 && Error() == 2); + Require(lchown_nid_postfix(dangling.c_str(), 0, 0) == 0); + Require(fchownat_nid_postfix(-100, dangling.c_str(), 0, 0, 0) == -1 && Error() == 2); + Require(fchownat_nid_postfix(-100, dangling.c_str(), 0, 0, 0x200) == 0); + Require(ThrowsNaming([&] { linkat_nid_postfix(-100, symbolic.c_str(), -100, other.c_str(), 0); }, "symbolic link itself")); + Require(linkat_nid_postfix(-100, symbolic.c_str(), -100, other.c_str(), 0x400) == 0); +#ifdef _WIN32 + Require((GetFileAttributesW(std::filesystem::path(other).c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) == 0); +#else + Require(!std::filesystem::is_symlink(std::filesystem::symlink_status(other))); +#endif + Require(std::filesystem::hard_link_count(file) == 3); + Require(std::filesystem::remove(other) && std::filesystem::remove(symbolic) && std::filesystem::remove(dangling)); + } + Require(sceKernelClose(directory) == 0); + Require(std::filesystem::remove(linked) && std::filesystem::remove(file)); +} + bool SymlinkTargetThrows(const char* target, const std::string& created) { try { symlink_nid_postfix(target, created.c_str()); @@ -277,6 +357,7 @@ int main() { std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); Require(std::filesystem::create_directory(root)); DirectoryRenameTests(root); + OwnershipAndLinkTests(root); const auto file = (root / "file.txt").string(); std::ofstream(file).put('x'); CheckPathconf(root, file); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 8a7b58c9c3..22df35e8d7 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -25,6 +25,8 @@ - [sceKernelGetModuleInfoInternal and sceKernelGetModuleListInternal](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) answer for every loaded module, system modules included. `sceKernelGetModuleInfoInternal` fills the 0x1A8-byte `SceKernelModuleInfoEx` of a module handle the way `sceKernelGetModuleInfoFromAddr` does for an address, with [shadPS4's checks](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/kernel/process.cpp#L243-L258) in their order: `EFAULT` for a null pointer, `EINVAL` when `st_size` is not 0x1A8, `ESRCH` for an unknown handle. `sceKernelGetModuleListInternal` returns the same list as `sceKernelGetModuleList`, as [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_libkernel.pas#L852-L858) does; fpPS4 marks that as a guess, and neither is checked on a PS5 - [chroot](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) follows FreeBSD 11.4's [`sys_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L776-L807) and [`pwd_chroot`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L3059-L3085) for a root caller: `EFAULT` for a null path, `ENOENT` for an empty or missing one, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255, and `ENOTDIR` for a path that is not a directory or passes through a file. A directory that is the guest root (`/`, `/..`, or `.` after `fchdir` to a descriptor of `/`) succeeds and changes nothing, as the root stays the same directory; the guest is taken never to have changed its root, so open directory descriptors do not make it fail with `EPERM` (`kern.chroot_allow_open_directories` 1). Any other directory throws, naming the path: the guest path resolver has a fixed root and mount aliases (`/app0`, `/savedata0`) that do not move with it. On the console a title starts in a sandbox root, where `pwd_chroot` refuses with `EPERM` while a directory is open; whether that holds for titles, and whether a title holds `PRIV_VFS_CHROOT`, are unverified. Porpoise (PPSA99764) weak-imports it and calls it only as `fchdir` to its saved descriptor of `/` then `chroot(".")`, to put its sandbox root back after a jailbreak daemon replaced it ([`put_root_back`](https://github.com/elripalda/Porpoise-Dolphin-Emulator-for-PS5/blob/2ed1ab5a06995aa198f7bccd4da0f90c3fc29d39/src/porpoise_jailbreak.cpp#L303-L331)) - [fchown](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled: the guest's own owner and group (0) and the unchanged value (-1) are accepted, a descriptor that is not open fails with `EBADF`, and any other owner or group throws, naming it. The check reads the descriptor with `fstat`, ignoring the CRT's invalid-parameter handler on Windows as `DescriptorControl` does. The console's ownership rules are unverified. Morrowind (`mshivam019/morrowind-ps5`) imports it +- [chown, lchown and fchownat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) - ownership is not modelled, as for `fchown`: owner and group 0 and the unchanged value (-1) are accepted, and any other owner or group throws, naming it. The path is looked up first, as in FreeBSD 11's `kern_fchownat`, with `stat`, with `lstat` for `lchown`, and with `fstatat` for `fchownat`, so a lookup error is returned before the owner is checked and nothing is changed on the host. `fchownat` flags other than `AT_SYMLINK_NOFOLLOW` (0x200) return `EINVAL`, as in FreeBSD 11. The console's ownership rules are unverified +- [linkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `link`, and flags other than `AT_SYMLINK_FOLLOW` (0x400) return `EINVAL`, as in FreeBSD 11. A relative path with a directory descriptor throws, and so does a symbolic link as the source without `AT_SYMLINK_FOLLOW`, where FreeBSD links the symbolic link itself. Console behaviour is unverified - [pthread_set_defaultstacksize_np and scePthreadSetDefaultstacksize](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) set the stack size that `scePthreadAttrInit` and a thread created without attributes use, as [fpPS4's `pthread_set_defaultstacksize_np`](https://github.com/red-prig/fpPS4/blob/04cefd4/kernel/ps4_pthread_attr.pas#L89-L102) does: a size of 16384 bytes or less fails with `EINVAL` (`0x80020016` from the sce form) and there is no upper limit, so a default the host cannot allocate fails only when a thread is created. Attributes initialised earlier keep their size. Not checked on a PS5 ### Unknown function info From 6ab7bd2ff5a9152154db9c7da472f6c20a7e0a15 Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:04:45 +0200 Subject: [PATCH 201/318] feat(shader): take shader disk cache format 46 for the 4_4_4_4 target merge main took format 45 for its barycentric metadata, so this branch takes the next number. Co-Authored-By: Claude Sonnet 5.5 --- core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index abd799b205..d007fc126a 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -13,7 +13,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 45; +inline constexpr std::uint32_t FormatVersion = 46; enum class LoadStatus { Loaded, From 7e217f1490cfb844ab332e693dcf47393d265be7 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:39:07 +0200 Subject: [PATCH 202/318] test(libkernel): run the guest_kqueue standard input case on a null device The case expects an EVFILT_WRITE change on descriptor 0 to throw because descriptor 0 is neither a socket nor the write end of a pipe. When the test is started with a pipe as standard input the guest cannot tell the ends of a Windows pipe apart, the change is accepted and the test aborts at that check. Point descriptor 0 at the null device first so the result no longer depends on how the test was started. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestKqueue.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 62085d4470..d7c73cad1e 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -15,6 +15,12 @@ #include #include #endif +#include +#ifdef _WIN32 +#include +#else +#include +#endif extern "C" { int APS5_VABI kqueue_nid_postfix(); @@ -1112,7 +1118,20 @@ static void Timeout() { Require(close_nid_postfix(kq) == 0); } +static void ReplaceStandardInputWithNull() { +#ifdef _WIN32 + const int null = _open("NUL", _O_RDONLY); + Require(null >= 0 && _dup2(null, 0) == 0); + Require(_close(null) == 0); +#else + const int null = ::open("/dev/null", O_RDONLY); + Require(null >= 0 && ::dup2(null, 0) == 0); + Require(::close(null) == 0); +#endif +} + static void StandardInput() { + ReplaceStandardInputWithNull(); const int copy = dup_nid_postfix(0); if (copy < 0) { Require(*__error_nid_postfix() == GuestEbadf); From 0aa3f3505b71897646d2562c55ea9cfebc15a891 Mon Sep 17 00:00:00 2001 From: xdCloudy <52116030+xdCloudy@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:47:28 +0100 Subject: [PATCH 203/318] feat(libSceCamera): declare PS5 3.20 pending exports --- core/libs/prx/libSceCamera/Export.cpp | 529 ++++++++++++++++++++++++++ docs/dev/debt/libSceCamera.md | 109 ++++++ 2 files changed, 638 insertions(+) diff --git a/core/libs/prx/libSceCamera/Export.cpp b/core/libs/prx/libSceCamera/Export.cpp index 26319c1e6c..2d7757d706 100644 --- a/core/libs/prx/libSceCamera/Export.cpp +++ b/core/libs/prx/libSceCamera/Export.cpp @@ -62,4 +62,533 @@ int APS5_VABI sceCameraIsValidFrameData(void) { return 0; } +int APS5_VABI sceCameraStopByHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDeviceIDWithoutOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraInitializeRegistryCalibData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetContrast(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Finalize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetConnectedDeviceAndDriverMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2IsAttached(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetSharpness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetTrackerMode(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetContrast(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetCalibDataForEve(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetCalibDataFromDevice(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetContrast(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraIsConnectedForSystem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetRegister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetVideoSyncInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetConfigInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetAttributeInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetMmapConnectedCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetSaturation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDeviceConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAudioOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetAutoExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetCalibDataFromDeviceForEve(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetVrCaptureInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetCalibDataForEve(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetLensCorrection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetCalibData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetProcessFocusByHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetSharpness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Initialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetHue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAccGetData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetAttribute(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetSaturation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetHue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Stop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetSaturation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetAutoWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAudioClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetProductInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetGamma(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetLensCorrection(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraChangeAppModuleState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetFrameData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetContrast(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetSaturation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetSharpness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraStartByHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Open(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Start(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetLensCorrectionInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraOpenByModuleId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetCalibrationData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAudioGetData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetHue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetGamma(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetCalibData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraWaitForEveReady(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetRegistryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAudioGetData2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetAppModuleFocus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetDefectivePixelCancellation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDeviceID(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetForceActivate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetFieldOfView(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2IsValidFrameData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetProcessFocus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetGamma(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetConfig(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDeviceConfigWithoutHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraDeviceOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetUacModeInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetAutoExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetRegister(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetGamma(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraRemoveAppModuleFocus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetAutoWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetDefectivePixelCancellation(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2Close(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetVideoSync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraWaitForLukeReady(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetDefectivePixelCancellationInternal(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2GetSharpness(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetAutoWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetDebugStop(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCamera2SetAutoExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraIsConfigChangeDone(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetVideoSync(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraAudioReset(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetAutoExposureGain(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraSetAutoWhiteBalance(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraGetHue(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCameraCloseByHandle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/docs/dev/debt/libSceCamera.md b/docs/dev/debt/libSceCamera.md index d0346584a7..955b1e157c 100644 --- a/docs/dev/debt/libSceCamera.md +++ b/docs/dev/debt/libSceCamera.md @@ -7,3 +7,112 @@ ### Functional - [sceCameraIsAttached](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera) - no camera is ever attached: index 0 returns 0 (not attached) and any other index returns `SCE_CAMERA_ERROR_PARAM` (0x802E0000), as [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/9a8ae4417bbfc79b1a43eb8a2eb0bb602148261e/src/core/libraries/camera/camera.cpp#L187-L200) does without a configured camera, with the error value from its [camera_error.h](https://github.com/shadps4-emu/shadPS4/blob/9a8ae4417bbfc79b1a43eb8a2eb0bb602148261e/src/core/libraries/camera/camera_error.h). Not measured on a console. Closes when: a host camera is exposed to guests. + +### Additional PS5 3.20 imports + +- [sceCameraStopByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `+X1Kgnn3bzg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetDeviceIDWithoutOpen](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `-H3UwGQvNZI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetAttribute](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `0btIPD5hg5A`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraInitializeRegistryCalibData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `0wnf2a60FqI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `1-IJHxzRJGw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Finalize](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `186Jz869gsc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetConnectedDeviceAndDriverMode](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `2dBOiluwiMw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2IsAttached](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `2v21-m4gljU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `2zCd8XDOe-Y`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetTrackerMode](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `3VJOpzKoIeM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `3i5MEzrC1pg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetCalibDataForEve](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `4IGO2LPD3h8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetCalibDataFromDevice](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `5Oie5RArfWs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `66IVWcdNHyI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraIsConnectedForSystem](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `68S3GKWhY6o`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `8MEjogxPrv0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetRegister](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `8MjO05qk5hA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetVideoSyncInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `8WtmqmE4edw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetConfigInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `9+SNhbctk64`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetAttributeInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `96F7zp1Xo+k`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `AfIDd+2ycTs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetMmapConnectedCount](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `B260o9pSzM8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `DAycoVmY3Mw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetDeviceConfig](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `DSOLCrc3Kh8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAudioOpen](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `DhqqFiBU+6g`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `FZkD-E6MNTQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetCalibDataFromDeviceForEve](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `H-7tWq8gfKs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `HX5524E5tMY`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetVrCaptureInfo](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `IAz2HgZQWzE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetCalibDataForEve](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `Jkz3xxMeyIM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetAttribute](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `JtD7ZG45rdw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetLensCorrection](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `K7W7H4ZRwbc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetCalibData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `LEMk5cTHKEA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetProcessFocusByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `NVITuK83Z7o`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetConfig](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `O5x-G9Rqwx4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ObIste7hqdk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `P-7MVfzvpsM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Initialize](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `PFRl8SmSW+4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `QI8GVJUy2ZY`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAccGetData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `QhjrPkRPUZQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetAttribute](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `Qk6kEJ0RJlM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetCalibrationData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `RHYJ7GKOSMg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `RTDOsWWqdME`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `T8jy0JWa210`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Stop](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `TZWR3p6XxXk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `TzkL-nUWfaQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `U8oNsRef+jI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAudioClose](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `UFonL7xopFM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetProductInfo](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ULxbwqiYYuU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `WVox2rwGuSc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetLensCorrection](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `XqYRHc4aw3w`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraChangeAppModuleState](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `Y0pCDajzkVQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetFrameData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `YlFCMjbyzh8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetContrast](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `a5xFueMZIMs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetSaturation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `bSKEi2PzzXI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `c6Fp9M1EXXc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `c9XZGDF1OcM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraStartByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `cLxF1QtHch0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Open](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `dmLUJh3bVTc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Start](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `eGkcUia48ts`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetLensCorrectionInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `eHa3vhGu2rQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraOpenByModuleId](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `eTywOSWsEiI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetCalibrationData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `emZBXv7cuAI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAudioGetData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `fkZE7Hup2ro`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `gB+OkFvkSXE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `gJqqsinextg`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetCalibData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hHA1frlMxYE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraWaitForEveReady](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hPVradh5wpo`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetRegistryInfo](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hawKak+Auw4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAudioGetData2](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `hftC5A1C8OQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetAppModuleFocus](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `j5isFVIlZLk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetDefectivePixelCancellation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `jMv40y2A23g`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetDeviceID](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `jTJCdyv9GLU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetForceActivate](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `jeTpU0MqKU0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `k3zPIcgFNv0`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetFieldOfView](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `kVWBITsSB3o`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2IsValidFrameData](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `lQ7rWWlOArI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetProcessFocus](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `lS0tM6n+Q5E`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `lhEIsHzB8r4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetConfig](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `mLRzKdIDwiU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `n+R7PGJa6MI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetDeviceConfigWithoutHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `n+rFeP1XXyM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraDeviceOpen](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `nBH6i2s4Glc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetUacModeInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `nnR7KAIDPv8`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `oEi6vM-3E2c`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetRegister](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `olojYZKYiYs`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetGamma](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `pfiqU2f6PQY`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraRemoveAppModuleFocus](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `py8p6kZcHmA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `qTPRMh4eY60`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetDefectivePixelCancellation](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `tslCukqFE+E`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2Close](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `uBRW3tEoWWM`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetVideoSync](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ubD+FitjqhA`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraWaitForLukeReady](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `v9XTZHXoJyQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetDefectivePixelCancellationInternal](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vER3cIMBHqI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2GetSharpness](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vPi3gSzw79M`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vS20kteXHdw`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetDebugStop](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vejouEusC7g`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCamera2SetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `vx9MQYicBVQ`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraIsConfigChangeDone](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wQfd7kfRZvo`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wgBMXJJA6K4`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetVideoSync](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wpeyFwJ+UEI`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraAudioReset](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `wyU98EXAYxU`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetAutoExposureGain](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `yfSdswDaElo`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraSetAutoWhiteBalance](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `zIKL4kZleuc`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraGetHue](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `zrIUDKZx0iE`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` +- [sceCameraCloseByHandle](../../../core/libs/prx/libSceCamera/Export.cpp) (libSceCamera, NID `ztqH5qNTpTk`) - listed in the [PS5 3.20 export inventory](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCamera.c); signature and behavior are unverified, so this parameterless declaration is for import resolution only and throws through `NotImplemented_nid_no_patch` From 1c026e5fee87e9e8d27be443d25472903da6bdf3 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 23 Oct 2026 04:17:45 -0400 Subject: [PATCH 204/318] fix(libkernel_sys): report ELOOP from link through a Windows symbolic link loop link took the source's std::filesystem::status first, and MinGW-w64's libstdc++ reports a Windows symbolic link loop as not found (ENOENT), so it returned ENOENT before FollowLinkPath, which maps the loop's ERROR_CANT_RESOLVE_FILENAME to ELOOP, was reached. guest_kernel_sys's loop case failed at that check wherever symbolic links can be created (Windows 11 22H2: "check failed at line 835 (guest errno 2)"). When the status reports the source missing, ask FollowLinkPath first: a loop now returns ELOOP and a dangling link still ENOENT; a missing path is unchanged. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel_sys/Export.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 53ab5bc867..53110cce05 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -398,7 +398,13 @@ int APS5_VABI link_nid_postfix(const char* existing, const char* created) { if (const int error = ResolvePathError_nid_no_patch(created, destination)) return static_cast(Fail(error)); std::error_code error; const auto status = std::filesystem::status(source, error); - if (!std::filesystem::exists(status)) return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + if (!std::filesystem::exists(status)) { +#ifdef _WIN32 + std::filesystem::path target; + if (const int followError = FollowLinkPath(source, target)) return static_cast(Fail(followError)); +#endif + return static_cast(Fail(HasNonDirectoryPrefix(source) ? GuestEnotdir : GuestEnoent)); + } if (std::filesystem::is_directory(status)) return static_cast(Fail(GuestEperm)); if (std::filesystem::exists(std::filesystem::symlink_status(destination, error))) return static_cast(Fail(GuestEexist)); if (!std::filesystem::is_directory(destination.parent_path(), error)) return static_cast(Fail(HasNonDirectoryPrefix(destination) ? GuestEnotdir : GuestEnoent)); From c1abb7eb4c2ee43de22d662c488176a3d190b792 Mon Sep 17 00:00:00 2001 From: thuanquang Date: Thu, 22 Oct 2026 00:25:43 +0700 Subject: [PATCH 205/318] fix(libc): reject a wcsncat_s copy only when its written and read ranges overlap Co-Authored-By: Claude --- core/libs/prx/libc/src/Strings.cpp | 4 ++-- core/libs/tests/GuestWideStrings.cpp | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index b3d173267a..1eed911f95 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -863,8 +863,8 @@ int APS5_VABI wcsncat_s_nid_postfix(char16_t* dest, size_t destsz, const char16_ } const auto destination = reinterpret_cast(dest + used); const auto source = reinterpret_cast(src); - const auto distance = destination < source ? source - destination : destination - source; - if (length != 0 && distance < (length + 1) * sizeof(char16_t)) { + const auto bytes = length * sizeof(char16_t); + if (destination < source + bytes && source < destination + bytes) { dest[0] = 0; return GuestEinval; } diff --git a/core/libs/tests/GuestWideStrings.cpp b/core/libs/tests/GuestWideStrings.cpp index 9137e19649..cc3bfc0ff5 100644 --- a/core/libs/tests/GuestWideStrings.cpp +++ b/core/libs/tests/GuestWideStrings.cpp @@ -240,6 +240,11 @@ int main() { wmemset_nid_postfix(strBuf, u'x', 4); require(wcsncat_s_nid_postfix(strBuf, 4, u"a", 1) == 34 && strBuf[0] == 0); wcsncpy_s_nid_postfix(strBuf, 16, u"abc", 3); + require(wcsncat_s_nid_postfix(strBuf, 16, strBuf + 1, 2) == 0 && same(strBuf, u"abcbc", 6)); + const char16_t splitSource[] = u"abc\0xyz"; + std::memcpy(strBuf, splitSource, sizeof(splitSource)); + require(wcsncat_s_nid_postfix(strBuf, 16, strBuf + 4, 3) == 22 && strBuf[0] == 0); + wcsncpy_s_nid_postfix(strBuf, 16, u"abc", 3); require(wcscat_s_nid_postfix(strBuf, 16, u"def") == 0 && same(strBuf, u"abcdef", 7)); wcsncpy_s_nid_postfix(strBuf, 8, u"12345", 5); require(wcscat_s_nid_postfix(strBuf, 8, u"6789") == 34 && strBuf[0] == 0); From e53f78214a970deeb6947c25716db7501fa6541a Mon Sep 17 00:00:00 2001 From: thuanquang Date: Fri, 23 Oct 2026 15:19:08 +0700 Subject: [PATCH 206/318] docs(libc): note where the wcsncat_s overlap rule comes from The debt entry says the rule follows a static reading of the console's wcsncat_s in libc.prx, that wcscat_s inherits it unread, and that the narrow strncat_s keeps a wider rule, as asked in review. Co-Authored-By: Claude --- docs/dev/debt/libc.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 3a606286a0..d9884f836b 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -37,6 +37,7 @@ - [Integer compiler-rt builtins](../../../core/libs/prx/libc/src/IntegerBuiltins.cpp) (libc) - the console traps in `__divsi3`, `__modsi3` and the 64- and 128-bit divisions on a zero divisor (`div`) and in `__negv*`, `__absv*` and `__mulv*` on overflow (an abort helper, then `ud2`); these throw instead. As in the `libc.prx` bundled with PPSA08427 (disassembly), `__addv*` and `__subv*` wrap without an overflow check, and `__udivsi3`, `__umodsi3` and `__udivmodsi4` divide in software, giving a quotient of 0 and the dividend as remainder for a zero divisor; `__clzdi2` and `__ctzdi2` of 0 return 32 and `__clzti2` and `__ctzti2` of 0 return 64 - [qsort](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows FreeBSD 11 [qsort.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libc/stdlib/qsort.c): Bentley and McIlroy's quicksort, with insertion sort below 7 elements and after a partition pass that swaps nothing, so elements that compare equal end in FreeBSD's order and the comparator is called in the same sequence. That the console's libc uses this FreeBSD code is unverified - [memcpy_s, memmove_s, strcpy_s, strncpy_s, strcat_s, strncat_s, wmemcpy_s, wmemmove_s, wcsncpy_s, wcscat_s, wcsncat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) use the C11 Annex K destination size constraints, with RSIZE_MAX taken as (SIZE_MAX >> 1) / sizeof(char16_t) for wide operations and SIZE_MAX >> 1 for byte operations. An oversized destination size returns ERANGE (34) without touching the destination; the console's RSIZE_MAX and exact error value are unverified. Constraint violations return an error without invoking the registered constraint handler, as before. +- [wcsncat_s and wcscat_s](../../../core/libs/prx/libc/src/Strings.cpp) (libc) refuse a copy as overlapping only when the written range `[dest + len1, dest + len1 + len2)` meets the read range `[src, src + len2)`. The rule comes from a static reading of the console's `wcsncat_s` in `libc.prx` (not run on a console); `wcscat_s` gets it through `wcsncat_s`, and its own console code was not read. The narrow `strncat_s` keeps a wider rule, `[dest, dest + used + length + 1)` against `[src, src + length + 1)`, whose console code was not read either, so it refuses some copies whose wide equivalent proceeds. - [snwprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: the handler installed with `set_constraint_handler_s` is not invoked, and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [swprintf_s and vswprintf_s](../../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follow C11 K.3.9.1.4 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`: the complete output, including the terminating null, must fit in the buffer, otherwise it is a runtime-constraint violation that sets `buffer[0]` to zero and returns a negative value, and the buffer is not partially written. Argument and encoding checks match `snwprintf_s` - [strerror_s and strerrorlen_s](../../../core/libs/prx/libc/src/WideConversion.cpp) (libc) follow [C11 K.3.7.4.2 and K.3.7.4.3](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf), using the existing FreeBSD error messages and unknown-error formatting. Truncation writes an ellipsis when capacity exceeds 3; constraint failures leave the destination untouched and invoke the registered guest-ABI handler. The console's `RSIZE_MAX` (`SIZE_MAX >> 1` here), nonzero returns (`EINVAL` 22 for constraints, `ERANGE` 34 for truncation), handler arguments and default abort behavior are unverified. Messages remain in the C locale, and guest errno is preserved. From 5b739d2b56f6b30ec0b6a1a7abbf178aba37094c Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:33:00 +0200 Subject: [PATCH 207/318] fix(libkernel): fail fchflags on a kqueue descriptor with EINVAL FreeBSD resolves the descriptor with getvnode in sys_fchflags (vfs_syscalls.c:2403), which fails with EINVAL for a descriptor that has no vnode. fchflags threw for a kqueue descriptor instead. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel/File/src/Stdio.cpp | 2 +- core/libs/tests/GuestFilesystem.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 82aa717df1..170e0f5b6b 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -706,7 +706,7 @@ int APS5_VABI chflags_nid_postfix(const char* path, std::uint64_t flags) { } int APS5_VABI fchflags_nid_postfix(int d, std::uint64_t flags) { - if (GuestSockets::IsOpen(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); + if (GuestSockets::IsOpen(d) || IsKqueueDescriptor(d) || File::IsSharedMemoryDescriptor_nid_no_patch(d)) return PosixFailure(GUEST_EINVAL); File::RefuseArchivedFile(d, "fchflags"); if (!File::IsVnode(d)) { if (!DescriptorIsOpen_nid_no_patch(d)) return PosixFailure(GUEST_EBADF); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 9e3fe832d1..f76230071e 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -39,6 +39,7 @@ int APS5_VABI fchmod_nid_postfix(int, int); int APS5_VABI chmod_nid_postfix(const char*, int); int APS5_VABI chflags_nid_postfix(const char*, std::uint64_t); int APS5_VABI fchflags_nid_postfix(int, std::uint64_t); +int APS5_VABI kqueue_nid_postfix(); int APS5_VABI futimes_nid_postfix(int, const KernelTimeval*); int APS5_VABI sceKernelFutimes(int, const KernelTimeval*); int APS5_VABI socket_nid_postfix(int, int, int); @@ -708,6 +709,10 @@ int main() { Require(fchflags_nid_postfix(flagged, 0) == -1 && *__error_nid_postfix() == 9); Require(fchflags_nid_postfix(flagged, ~0ULL) == -1 && *__error_nid_postfix() == 9); Require(fchflags_nid_postfix(-1, 0) == -1 && *__error_nid_postfix() == 9); + const int flaggedKqueue = kqueue_nid_postfix(); + Require(flaggedKqueue >= 0); + Require(fchflags_nid_postfix(flaggedKqueue, 0) == -1 && *__error_nid_postfix() == 22); + Require(close_nid_postfix(flaggedKqueue) == 0); const int flaggedRoot = sceKernelOpen(root.string().c_str(), 0x20000, 0); Require(flaggedRoot >= 0 && fchflags_nid_postfix(flaggedRoot, 0) == 0 && sceKernelClose(flaggedRoot) == 0); int flaggedPipe[2]; From 194c03afde5d9eb8c05b13361597fad19036be0d Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Tue, 20 Oct 2026 16:01:31 +0200 Subject: [PATCH 208/318] fix(agc): order deferred host-import settles with main-queue accesses --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../Graphics/src/GuestBufferMemory.cpp | 33 ++----- .../prx/libSceAgcDriver/tests/Recorder.cpp | 94 ++++++++++++++++--- 3 files changed, 93 insertions(+), 39 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8d92618a22..8e0654bc29 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1489,6 +1489,11 @@ if(BUILD_TESTING) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_import_settle_deferred COMMAND agc_driver_recorder_tests --deferred-settle-only) add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) + add_test(NAME agc_import_settle_write_order COMMAND agc_driver_recorder_tests --settle-write-order-only) + add_test(NAME agc_import_settle_write_order_other_family COMMAND agc_driver_recorder_tests --settle-write-order-other-family-only) + set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family PROPERTIES + SKIP_RETURN_CODE 77 TIMEOUT 30 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 929bd02e0d..2a5f5d4b45 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -293,7 +293,7 @@ HostImports& Imports() { #ifdef _WIN32 void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { - if (batch.busy && settle.waitForFences != nullptr) static_cast(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 10'000'000'000ull)); + if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences (settle batch)"); } bool settleBatchRunning(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { @@ -987,7 +987,7 @@ const char* prepareSettleBatch(const Context& context, ImportSettle& settle, Set } if (batch.commands == VK_NULL_HANDLE) { VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; - allocate.commandPool = context.settlePool; + allocate.commandPool = context.pool; allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocate.commandBufferCount = 1; if ((result = context.Function("vkAllocateCommandBuffers")(context.device, &allocate, &batch.commands)) != VK_SUCCESS) { @@ -1037,9 +1037,7 @@ void submitSettleBatch(const Context& context, HostImports& state) { begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; if ((result = context.Function("vkBeginCommandBuffer")(batch.commands, &begin)) != VK_SUCCESS) return fail("vkBeginCommandBuffer (settle batch)"); const auto copy = context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer"); - const bool otherFamily = context.settleFamily != context.queueFamily; - const auto createBuffer = otherFamily ? context.Function("vkCreateBuffer") : nullptr; - const auto bindBufferMemory = otherFamily ? context.Function("vkBindBufferMemory") : nullptr; + RecordMemoryBarrier(context, batch.commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); std::size_t consumed = 0; for (; consumed < state.settleWanted.size() && batch.bases.size() < settleBatchCopies; ++consumed) { const auto base = state.settleWanted[consumed]; @@ -1050,38 +1048,19 @@ void submitSettleBatch(const Context& context, HostImports& state) { state.pendingSettles.erase(settle); continue; } - VkBuffer source = import->second.buffer; - if (otherFamily) { - VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO}; - external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; - VkBufferCreateInfo aliasInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; - aliasInfo.size = std::min(import->second.bytes, 65536); - aliasInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; - VkBuffer alias = VK_NULL_HANDLE; - if (createBuffer(context.device, &aliasInfo, nullptr, &alias) != VK_SUCCESS) { - state.pendingSettles.erase(settle); - continue; - } - if (bindBufferMemory(context.device, alias, import->second.memory, 0) != VK_SUCCESS) { - destroyBuffer(context.device, alias, nullptr); - state.pendingSettles.erase(settle); - continue; - } - batch.aliases.push_back(alias); - source = alias; - } const VkBufferCopy region{0, (VkDeviceSize{index} * settleBatchCopies + batch.bases.size()) * 4u, 4}; - copy(batch.commands, source, deviceSettle.batchTarget, 1, ®ion); + copy(batch.commands, import->second.buffer, deviceSettle.batchTarget, 1, ®ion); settle->second = PendingSettle{SettlePhase::Submitted, index, context.device}; batch.bases.push_back(base); } state.settleWanted.erase(state.settleWanted.begin(), state.settleWanted.begin() + static_cast(consumed)); + RecordMemoryBarrier(context, batch.commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_WRITE_BIT); if ((result = context.Function("vkEndCommandBuffer")(batch.commands)) != VK_SUCCESS) return fail("vkEndCommandBuffer (settle batch)"); if (batch.bases.empty()) return; VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; submit.pCommandBuffers = &batch.commands; - if ((result = context.Function("vkQueueSubmit")(context.settleQueue, 1, &submit, batch.fence)) != VK_SUCCESS) return fail("vkQueueSubmit (settle batch)"); + if ((result = context.Function("vkQueueSubmit")(context.queue, 1, &submit, batch.fence)) != VK_SUCCESS) return fail("vkQueueSubmit (settle batch)"); batch.busy = true; state.settlesSubmitted += batch.bases.size(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 68a362406c..63ce16a1e0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2027,7 +2027,10 @@ bool settleFirstUseRaced(const Context& context, int copies) { const auto* import = HostImportFor(context, address, unit); Require(import != nullptr && import->unwatched && !Watched(address, unit), "(f) a 64 KiB import was not compared at first"); const auto commands = recorder.Commands(); - for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, import->buffer, 0, scratch.Handle(), 0, unit); + for (int copy = 0; copy < copies; ++copy) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + CopyBuffer(context, commands, import->buffer, 0, scratch.Handle(), 0, unit); + } recorder.Submit(); const auto submittedAt = std::chrono::steady_clock::now(); const auto deadline = submittedAt + std::chrono::seconds(5); @@ -2066,6 +2069,69 @@ bool settleFirstUseRaced(const Context& context, int copies) { return racing; } +bool settleWriteOrderTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: settle write order not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: settle write order not tested\n"; + return false; + } + void* block = AllocateWatched(unit, unit); + struct Release { + void* block; + ~Release() { ReleaseWatched(block, unit); } + } release{block}; + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, unit, true, true, true); + } + Recorder recorder(context); + recorder.Activate(); + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched, "the write-order import was not initially compared"); + const auto fill = [&](std::uint32_t value) { + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdFillBuffer")(commands, import->buffer, HostImportOffset(*import, address), unit, value); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + recorder.NotePendingWrite(address, unit); + recorder.Submit(); + }; + const auto before = HostImportSettles().submitted; + fill(0x22222222u); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (HostImportSettles().submitted == before && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(HostImportFor(context, address, unit) == import, "the write-order import changed while settling"); + } + Require(HostImportSettles().submitted > before, "the write-order import did not submit a settle"); + fill(0x33333333u); + recorder.Sync(); + while (!Watched(address, unit) && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + Require(Watched(address, unit), "the write-order import did not return to the write watch"); + const auto* bytes = static_cast(block); + Require(std::all_of(bytes, bytes + unit, [](std::uint8_t value) { return value == 0x33; }), "the GPU store after the settle did not preserve its bytes"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, unit); + std::cout << "settle read between main-queue GPU writes passed\n"; + return true; +} + + bool settleFirstUseTests(const Device& device) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); @@ -2080,15 +2146,9 @@ bool settleFirstUseTests(const Device& device) { std::cout << "this device compares every Windows import: the first use of a settled import not tested\n"; return false; } - for (int copies = 4096; copies <= 65536; copies *= 4) { - if (settleFirstUseRaced(context, copies)) { - std::cout << "settled import first use tests passed\n"; - return true; - } - std::cout << "(f) the main-queue copies had finished before the import returned to the write watch, so its read did not race them\n"; - } - std::cout << "(f) the main-queue copies finished first in every round: the first use of a settled import not tested\n"; - return false; + Require(!settleFirstUseRaced(context, 4096), "(f) the import returned to the write watch while its earlier main-queue reads were still in flight"); + std::cout << "settled import first use ordering tests passed\n"; + return true; } bool settleEarlierImportTests(const Device& device) { @@ -2127,7 +2187,10 @@ bool settleEarlierImportTests(const Device& device) { const auto* earlier = HostImportFor(context, address, 16); Require(earlier != nullptr && earlier->base == address && earlier->bytes == unit && earlier->unwatched, "(e) the first window of the range was not imported compared"); const auto commands = recorder.Commands(); - for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, earlier->buffer, 0, scratch.Handle(), 0, unit); + for (int copy = 0; copy < copies; ++copy) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + CopyBuffer(context, commands, earlier->buffer, 0, scratch.Handle(), 0, unit); + } const auto* whole = HostImportFor(context, address + unit - 16, 32); Require(whole != nullptr && whole->base == address && whole->bytes == 2 * unit && whole->unwatched, "(e) an access across the windows did not replace the first window's import"); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); @@ -10374,7 +10437,7 @@ int main(int argc, char** argv) { } std::unique_ptr created; try { - created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only")); + created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only" || std::string_view(argv[1]) == "--settle-write-order-other-family-only")); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; @@ -10413,6 +10476,13 @@ int main(int argc, char** argv) { } return deferredSettleTests(device) ? 0 : 77; } + if (argc == 2 && (std::string_view(argv[1]) == "--settle-write-order-only" || std::string_view(argv[1]) == "--settle-write-order-other-family-only")) { + if (std::string_view(argv[1]) == "--settle-write-order-other-family-only" && device.GetContext().settleFamily == device.GetContext().queueFamily) { + std::cout << "skipped, no other queue family to settle imports on\n"; + return 77; + } + return settleWriteOrderTests(device) ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; From 9d4a0ebfcd9faf6c56151cda430ebda8e2ddd2ba Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Tue, 20 Oct 2026 16:33:21 +0200 Subject: [PATCH 209/318] docs(agc): describe deferred settle submission ordering --- docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 8676f9acff..77bf4fd9af 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -145,7 +145,7 @@ flowchart LR - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). - A guest protect or unmap that overlaps a plain host-pointer import retires the whole import before changing the guest range (`WaitForLeasesAndImports` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). Later accesses import surviving registered ranges again; imports never keep the pages across a cut pinned in a trimmed state. `GuestAllocations::Range::backingSerial` distinguishes the original pages from replacements: untouched pieces of a split keep their serial, newly mapped or protected pieces get a fresh one, and every span passed to `RequireUnpinned` (including the other mappings of remapped direct memory and the resident runs touched by a 2 MiB unmap or remap) gets a fresh serial when the mutation ends. - A draw binds its vertex and index ranges in place from a host import, except with NVIDIA's Windows driver, which reads a host import of guest memory far slower than a buffer it allocated (Astro Bot at Crash Site on an RTX 5070 Ti: 164 against 45 ms of draws per frame), so they are copied there (`DrawInputsInPlaceUsable`, `Context::drawInputsInPlace`). `APS5_DRAW_INPUTS_IN_PLACE=1` binds them in place and `APS5_NO_DRAW_INPUTS_IN_PLACE=1` copies them on any device; the second wins. -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the main queue, with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. ### Log output diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7569ec1c0f..0652db2d6e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -113,7 +113,8 @@ - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. -- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. +- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) whose protection an import resolves count as stored only when their bytes change. A same-value CPU store during that window cannot be distinguished from driver activity and does not win over pending results. On Windows, NVIDIA 617.42 marks every private MEM_WRITE_WATCH page after import until a GPU use completes. The driver behavior was measured on this driver; other drivers are unverified. Imports of at least 1 MiB wait for a four-byte read on the separate settle queue inside the window. Smaller imports stay compared until a deferred read completes; a later lookup returns whole blocks to write watching with a new CPU-store stamp. Blocks overlapping a live storage image, blocks of an import retired before rewatching, and imports without a settle queue stay compared. Shared-memory imports retain their separate-alias tracking. +- [Deferred host-import reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) use the main queue with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU. The latency cost of placing batches behind main-queue work is unmeasured. The first-use test now checks that earlier main-queue reads finish before the import returns to write watching; the original two-queue canary measured driver marking while those reads were still in flight. The earlier-import canary still checks later use of another import of the same pages. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. From a0ecec3d3b5c58c33587bd0a6f2a543ffd7ac545 Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Fri, 23 Oct 2026 13:10:52 +0200 Subject: [PATCH 210/318] fix(agc): bound deferred settle retirement waits --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Graphics/src/GuestBufferMemory.cpp | 2 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 109 ++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 8e0654bc29..3ff1462fea 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1491,8 +1491,10 @@ if(BUILD_TESTING) add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) add_test(NAME agc_import_settle_write_order COMMAND agc_driver_recorder_tests --settle-write-order-only) add_test(NAME agc_import_settle_write_order_other_family COMMAND agc_driver_recorder_tests --settle-write-order-other-family-only) - set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family PROPERTIES + add_test(NAME agc_import_settle_wait_failure COMMAND agc_driver_recorder_tests --settle-wait-failure-only) + set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family agc_import_settle_wait_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 + FAIL_REGULAR_EXPRESSION "SYNC-HAZARD-|VUID-" ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 2a5f5d4b45..6ceb8a9f43 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -293,7 +293,7 @@ HostImports& Imports() { #ifdef _WIN32 void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { - if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences (settle batch)"); + if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 60'000'000'000ull), "vkWaitForFences (settle batch)"); } bool settleBatchRunning(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 63ce16a1e0..86aa5f691a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2131,6 +2131,114 @@ bool settleWriteOrderTests(const Device& device) { return true; } +struct SettleWaitFailure { + PFN_vkGetDeviceProcAddr resolve; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkResult result = VK_SUCCESS; + std::uint64_t timeout = 0; + unsigned waits = 0; + unsigned destroys = 0; + unsigned frees = 0; +}; + +SettleWaitFailure* settleWaitFailure = nullptr; + +VKAPI_ATTR VkResult VKAPI_CALL settleWaitForFences(VkDevice device, std::uint32_t count, const VkFence* fences, VkBool32 all, std::uint64_t timeout) { + auto& failure = *settleWaitFailure; + if (failure.result != VK_SUCCESS) { + failure.timeout = timeout; + ++failure.waits; + return failure.result; + } + return reinterpret_cast(failure.resolve(device, "vkWaitForFences"))(device, count, fences, all, timeout); +} + +VKAPI_ATTR void VKAPI_CALL settleDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + auto& failure = *settleWaitFailure; + if (buffer == failure.buffer) ++failure.destroys; + reinterpret_cast(failure.resolve(device, "vkDestroyBuffer"))(device, buffer, allocator); +} + +VKAPI_ATTR void VKAPI_CALL settleFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + auto& failure = *settleWaitFailure; + if (memory == failure.memory) ++failure.frees; + reinterpret_cast(failure.resolve(device, "vkFreeMemory"))(device, memory, allocator); +} + +PFN_vkVoidFunction VKAPI_CALL settleWaitDeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkWaitForFences") == 0) return reinterpret_cast(settleWaitForFences); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast(settleDestroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(settleFreeMemory); + return settleWaitFailure->resolve(device, name); +} + +bool settleWaitFailureTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + auto context = device.GetContext(); + constexpr std::size_t unit = 65536; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) return false; + std::lock_guard gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) return false; + ClearHostImports(context.device); + SettleWaitFailure failure{context.deviceProc}; + settleWaitFailure = &failure; + context.deviceProc = settleWaitDeviceProc; + context.functions = nullptr; + void* block = AllocateWatched(unit, unit); + Require(block != nullptr, "no watched block for settle wait failure"); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, unit, true, true, true); + } + struct Release { + Context& context; + SettleWaitFailure& failure; + void* block; + ~Release() { + failure.result = VK_SUCCESS; + ClearHostImports(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ReleaseWatched(block, unit); + settleWaitFailure = nullptr; + } + } release{context, failure, block}; + const auto address = reinterpret_cast(block); + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched, "wait failure import was not initially compared"); + failure.buffer = import->buffer; + failure.memory = import->memory; + const auto before = HostImportSettles().submitted; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (HostImportSettles().submitted == before && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + Require(HostImportSettles().submitted > before, "wait failure did not submit a settle"); + Check(context.Function("vkDeviceWaitIdle")(context.device), "vkDeviceWaitIdle before settle failure"); + for (const auto result : {VK_TIMEOUT, VK_ERROR_DEVICE_LOST}) { + failure.result = result; + const auto waits = failure.waits; + std::string error; + try { + ClearHostImports(context.device); + } catch (const std::runtime_error& caught) { + error = caught.what(); + } + failure.result = VK_SUCCESS; + Require(failure.waits == waits + 1 && failure.timeout > 0 && failure.timeout <= 60'000'000'000ull, "settle retirement did not use a bounded fence wait"); + Require(error == "AGC graphics: vkWaitForFences (settle batch): Vulkan result " + std::to_string(result), "settle retirement did not propagate its Vulkan failure"); + Require(failure.destroys == 0 && failure.frees == 0 && HostImportCovers(context, address, unit), "failed settle wait released its host import"); + } + ClearHostImports(context.device); + Require(failure.destroys == 1 && failure.frees == 1, "successful settle retirement did not release the retained import exactly once"); + std::cout << "bounded settle wait preserves imports on timeout and device loss; successful retry releases once\n"; + return true; +} bool settleFirstUseTests(const Device& device) { using namespace AgcDriver::GuestMemory; @@ -10483,6 +10591,7 @@ int main(int argc, char** argv) { } return settleWriteOrderTests(device) ? 0 : 77; } + if (argc == 2 && std::string_view(argv[1]) == "--settle-wait-failure-only") return settleWaitFailureTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; From 54747c31fb750037b5792431dd3c744d91b37285 Mon Sep 17 00:00:00 2001 From: Th3EvilGod <117441689+Th3EvilGod@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:12:08 +0400 Subject: [PATCH 211/318] fix(rudp): use precise poll deadlines on Windows --- core/libs/prx/libSceRudp/Export.cpp | 13 ++++++------ core/libs/tests/GuestRudpPoll.cpp | 32 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceRudp/Export.cpp b/core/libs/prx/libSceRudp/Export.cpp index dcfb2c25be..ce37e56c0e 100644 --- a/core/libs/prx/libSceRudp/Export.cpp +++ b/core/libs/prx/libSceRudp/Export.cpp @@ -1,6 +1,4 @@ #include -#include -#include #include #include #include @@ -11,6 +9,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" // Offline reliable-UDP: the library initialises and contexts can be created and bound, but no connection can // ever be established because there is no network. @@ -96,7 +95,7 @@ struct RudpPoll { int g_next_poll = 1; std::map g_polls; -std::condition_variable g_polls_changed; +TimedWait::Condition g_polls_changed; bool valid_ctx(int ctx) { return g_ctx.count(ctx) != 0; @@ -157,7 +156,7 @@ int APS5_VABI sceRudpEnd(void) { g_inited = false; g_ctx.clear(); g_polls.clear(); - g_polls_changed.notify_all(); + g_polls_changed.NotifyAll(); return 0; } @@ -451,8 +450,8 @@ int APS5_VABI sceRudpPollWait(int pollId, RudpPollEvent* events, std::size_t eve } const auto closed = [pollId] { return !g_inited || g_polls.count(pollId) == 0; }; if (timeout > RUDP_POLL_WAIT_MAX_USEC) { - g_polls_changed.wait(lk, closed); - } else if (!g_polls_changed.wait_for(lk, std::chrono::microseconds(timeout), closed)) { + g_polls_changed.Wait(lk, closed); + } else if (!g_polls_changed.WaitUntil(lk, TimedWait::DeadlineNanos(timeout), closed)) { return 0; } return g_inited ? RUDP_ERROR_INVALID_POLL_ID : RUDP_ERROR_NOT_INITIALIZED; @@ -469,7 +468,7 @@ int APS5_VABI sceRudpPollDestroy(int pollId) { if (g_polls.erase(pollId) == 0) { return RUDP_ERROR_INVALID_POLL_ID; } - g_polls_changed.notify_all(); + g_polls_changed.NotifyAll(); return 0; } diff --git a/core/libs/tests/GuestRudpPoll.cpp b/core/libs/tests/GuestRudpPoll.cpp index 4ff92c482f..a76e793be5 100644 --- a/core/libs/tests/GuestRudpPoll.cpp +++ b/core/libs/tests/GuestRudpPoll.cpp @@ -1,9 +1,11 @@ #include "SceTypes.hpp" +#include #include #include #include #include +#include #include #include #include @@ -92,6 +94,23 @@ int main() { Require(sceRudpPollWait(poll, events.data(), eventCount, 20000) == 0); Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(20)); +#ifdef _WIN32 + for (const auto timeout : {100ULL, 1000ULL}) { + std::array samples{}; + for (auto& sample : samples) { + const auto before = std::chrono::steady_clock::now(); + Require(sceRudpPollWait(poll, events.data(), eventCount, timeout) == 0); + sample = std::chrono::steady_clock::now() - before; + Require(sample >= std::chrono::microseconds(timeout)); + } + std::sort(samples.begin(), samples.end()); + const auto median = samples[samples.size() / 2]; + std::fprintf(stderr, "sceRudpPollWait(%llu us) median: %lld ns\n", timeout, + static_cast(median.count())); + Require(median < std::chrono::milliseconds(12)); + } +#endif + Require(sceRudpTerminate(second) == 0); Require(sceRudpPollControl(poll, add, first, read) == 0); Require(sceRudpTerminate(first) == 0); @@ -101,6 +120,19 @@ int main() { Require(sceRudpPollControl(poll, add, third, read) == 0); Require(sceRudpPollControl(other, add, third, read) == 0); + const int finitePoll = sceRudpPollCreate(1); + Require(finitePoll > 0); + int finiteResult = 0; + std::thread finiteWaiter([&] { + finiteResult = sceRudpPollWait(finitePoll, events.data(), eventCount, 1000000); + }); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + const auto destroyedAt = std::chrono::steady_clock::now(); + Require(sceRudpPollDestroy(finitePoll) == 0); + finiteWaiter.join(); + Require(finiteResult == invalidPoll); + Require(std::chrono::steady_clock::now() - destroyedAt < std::chrono::milliseconds(500)); + int destroyedResult = 0; std::thread destroyedWaiter([&] { destroyedResult = sceRudpPollWait(poll, events.data(), eventCount, forever); }); std::this_thread::sleep_for(std::chrono::milliseconds(20)); From 961493dcc49548bde626220aebd059e748407124 Mon Sep 17 00:00:00 2001 From: Th3EvilGod <117441689+Th3EvilGod@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:16:41 +0400 Subject: [PATCH 212/318] fix(rudp): compile timed waits into the runtime library --- core/libs/prx/libSceRudp/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 core/libs/prx/libSceRudp/CMakeLists.txt diff --git a/core/libs/prx/libSceRudp/CMakeLists.txt b/core/libs/prx/libSceRudp/CMakeLists.txt new file mode 100644 index 0000000000..962bd86e42 --- /dev/null +++ b/core/libs/prx/libSceRudp/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(libSceRudp SHARED EXCLUDE_FROM_ALL + ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../libkernel/Time/TimedWait.cpp +) +target_include_directories(libSceRudp PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceRudp) From ac8be901adcac171265288f28e03b802d728dfbe Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Tue, 20 Oct 2026 16:01:31 +0200 Subject: [PATCH 213/318] fix(agc): order deferred host-import settles with main-queue accesses --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 + .../Graphics/src/GuestBufferMemory.cpp | 33 ++----- .../prx/libSceAgcDriver/tests/Recorder.cpp | 94 ++++++++++++++++--- 3 files changed, 93 insertions(+), 39 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7e7d22b021..e966e93c15 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1489,6 +1489,11 @@ if(BUILD_TESTING) set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77) add_test(NAME agc_import_settle_deferred COMMAND agc_driver_recorder_tests --deferred-settle-only) add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) + add_test(NAME agc_import_settle_write_order COMMAND agc_driver_recorder_tests --settle-write-order-only) + add_test(NAME agc_import_settle_write_order_other_family COMMAND agc_driver_recorder_tests --settle-write-order-other-family-only) + set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family PROPERTIES + SKIP_RETURN_CODE 77 TIMEOUT 30 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 0c7f40cd5c..45ea9b5901 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -293,7 +293,7 @@ HostImports& Imports() { #ifdef _WIN32 void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { - if (batch.busy && settle.waitForFences != nullptr) static_cast(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 10'000'000'000ull)); + if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences (settle batch)"); } bool settleBatchRunning(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { @@ -987,7 +987,7 @@ const char* prepareSettleBatch(const Context& context, ImportSettle& settle, Set } if (batch.commands == VK_NULL_HANDLE) { VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; - allocate.commandPool = context.settlePool; + allocate.commandPool = context.pool; allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocate.commandBufferCount = 1; if ((result = context.Function("vkAllocateCommandBuffers")(context.device, &allocate, &batch.commands)) != VK_SUCCESS) { @@ -1037,9 +1037,7 @@ void submitSettleBatch(const Context& context, HostImports& state) { begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; if ((result = context.Function("vkBeginCommandBuffer")(batch.commands, &begin)) != VK_SUCCESS) return fail("vkBeginCommandBuffer (settle batch)"); const auto copy = context.Resolved(&DeviceFunctions::cmdCopyBuffer, "vkCmdCopyBuffer"); - const bool otherFamily = context.settleFamily != context.queueFamily; - const auto createBuffer = otherFamily ? context.Function("vkCreateBuffer") : nullptr; - const auto bindBufferMemory = otherFamily ? context.Function("vkBindBufferMemory") : nullptr; + RecordMemoryBarrier(context, batch.commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT); std::size_t consumed = 0; for (; consumed < state.settleWanted.size() && batch.bases.size() < settleBatchCopies; ++consumed) { const auto base = state.settleWanted[consumed]; @@ -1050,38 +1048,19 @@ void submitSettleBatch(const Context& context, HostImports& state) { state.pendingSettles.erase(settle); continue; } - VkBuffer source = import->second.buffer; - if (otherFamily) { - VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO}; - external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_HOST_ALLOCATION_BIT_EXT; - VkBufferCreateInfo aliasInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, &external}; - aliasInfo.size = std::min(import->second.bytes, 65536); - aliasInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; - VkBuffer alias = VK_NULL_HANDLE; - if (createBuffer(context.device, &aliasInfo, nullptr, &alias) != VK_SUCCESS) { - state.pendingSettles.erase(settle); - continue; - } - if (bindBufferMemory(context.device, alias, import->second.memory, 0) != VK_SUCCESS) { - destroyBuffer(context.device, alias, nullptr); - state.pendingSettles.erase(settle); - continue; - } - batch.aliases.push_back(alias); - source = alias; - } const VkBufferCopy region{0, (VkDeviceSize{index} * settleBatchCopies + batch.bases.size()) * 4u, 4}; - copy(batch.commands, source, deviceSettle.batchTarget, 1, ®ion); + copy(batch.commands, import->second.buffer, deviceSettle.batchTarget, 1, ®ion); settle->second = PendingSettle{SettlePhase::Submitted, index, context.device}; batch.bases.push_back(base); } state.settleWanted.erase(state.settleWanted.begin(), state.settleWanted.begin() + static_cast(consumed)); + RecordMemoryBarrier(context, batch.commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_WRITE_BIT); if ((result = context.Function("vkEndCommandBuffer")(batch.commands)) != VK_SUCCESS) return fail("vkEndCommandBuffer (settle batch)"); if (batch.bases.empty()) return; VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO}; submit.commandBufferCount = 1; submit.pCommandBuffers = &batch.commands; - if ((result = context.Function("vkQueueSubmit")(context.settleQueue, 1, &submit, batch.fence)) != VK_SUCCESS) return fail("vkQueueSubmit (settle batch)"); + if ((result = context.Function("vkQueueSubmit")(context.queue, 1, &submit, batch.fence)) != VK_SUCCESS) return fail("vkQueueSubmit (settle batch)"); batch.busy = true; state.settlesSubmitted += batch.bases.size(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 851b36e404..667b46c27f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2027,7 +2027,10 @@ bool settleFirstUseRaced(const Context& context, int copies) { const auto* import = HostImportFor(context, address, unit); Require(import != nullptr && import->unwatched && !Watched(address, unit), "(f) a 64 KiB import was not compared at first"); const auto commands = recorder.Commands(); - for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, import->buffer, 0, scratch.Handle(), 0, unit); + for (int copy = 0; copy < copies; ++copy) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + CopyBuffer(context, commands, import->buffer, 0, scratch.Handle(), 0, unit); + } recorder.Submit(); const auto submittedAt = std::chrono::steady_clock::now(); const auto deadline = submittedAt + std::chrono::seconds(5); @@ -2066,6 +2069,69 @@ bool settleFirstUseRaced(const Context& context, int copies) { return racing; } +bool settleWriteOrderTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + const auto& context = device.GetContext(); + constexpr std::size_t unit = 65536; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) { + std::cout << "host imports within 64 KiB, a settle queue or write watching unavailable: settle write order not tested\n"; + return false; + } + std::unique_lock gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) { + std::cout << "this device compares every Windows import: settle write order not tested\n"; + return false; + } + void* block = AllocateWatched(unit, unit); + struct Release { + void* block; + ~Release() { ReleaseWatched(block, unit); } + } release{block}; + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, unit, true, true, true); + } + Recorder recorder(context); + recorder.Activate(); + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched, "the write-order import was not initially compared"); + const auto fill = [&](std::uint32_t value) { + const auto commands = recorder.Commands(); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdFillBuffer")(commands, import->buffer, HostImportOffset(*import, address), unit, value); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT); + recorder.NotePendingWrite(address, unit); + recorder.Submit(); + }; + const auto before = HostImportSettles().submitted; + fill(0x22222222u); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (HostImportSettles().submitted == before && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(HostImportFor(context, address, unit) == import, "the write-order import changed while settling"); + } + Require(HostImportSettles().submitted > before, "the write-order import did not submit a settle"); + fill(0x33333333u); + recorder.Sync(); + while (!Watched(address, unit) && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + Require(Watched(address, unit), "the write-order import did not return to the write watch"); + const auto* bytes = static_cast(block); + Require(std::all_of(bytes, bytes + unit, [](std::uint8_t value) { return value == 0x33; }), "the GPU store after the settle did not preserve its bytes"); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, address, unit); + std::cout << "settle read between main-queue GPU writes passed\n"; + return true; +} + + bool settleFirstUseTests(const Device& device) { using namespace AgcDriver::GuestMemory; const auto& context = device.GetContext(); @@ -2080,15 +2146,9 @@ bool settleFirstUseTests(const Device& device) { std::cout << "this device compares every Windows import: the first use of a settled import not tested\n"; return false; } - for (int copies = 4096; copies <= 65536; copies *= 4) { - if (settleFirstUseRaced(context, copies)) { - std::cout << "settled import first use tests passed\n"; - return true; - } - std::cout << "(f) the main-queue copies had finished before the import returned to the write watch, so its read did not race them\n"; - } - std::cout << "(f) the main-queue copies finished first in every round: the first use of a settled import not tested\n"; - return false; + Require(!settleFirstUseRaced(context, 4096), "(f) the import returned to the write watch while its earlier main-queue reads were still in flight"); + std::cout << "settled import first use ordering tests passed\n"; + return true; } bool settleEarlierImportTests(const Device& device) { @@ -2127,7 +2187,10 @@ bool settleEarlierImportTests(const Device& device) { const auto* earlier = HostImportFor(context, address, 16); Require(earlier != nullptr && earlier->base == address && earlier->bytes == unit && earlier->unwatched, "(e) the first window of the range was not imported compared"); const auto commands = recorder.Commands(); - for (int copy = 0; copy < copies; ++copy) CopyBuffer(context, commands, earlier->buffer, 0, scratch.Handle(), 0, unit); + for (int copy = 0; copy < copies; ++copy) { + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + CopyBuffer(context, commands, earlier->buffer, 0, scratch.Handle(), 0, unit); + } const auto* whole = HostImportFor(context, address + unit - 16, 32); Require(whole != nullptr && whole->base == address && whole->bytes == 2 * unit && whole->unwatched, "(e) an access across the windows did not replace the first window's import"); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); @@ -10482,7 +10545,7 @@ int main(int argc, char** argv) { } std::unique_ptr created; try { - created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only")); + created = std::make_unique(argc == 2 && (std::string_view(argv[1]) == "--import-watch-other-family-only" || std::string_view(argv[1]) == "--deferred-settle-other-family-only" || std::string_view(argv[1]) == "--host-import-lifetime-other-family-only" || std::string_view(argv[1]) == "--settle-write-order-other-family-only")); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::cout << "skipped, no usable Vulkan device: " << error.what() << '\n'; @@ -10521,6 +10584,13 @@ int main(int argc, char** argv) { } return deferredSettleTests(device) ? 0 : 77; } + if (argc == 2 && (std::string_view(argv[1]) == "--settle-write-order-only" || std::string_view(argv[1]) == "--settle-write-order-other-family-only")) { + if (std::string_view(argv[1]) == "--settle-write-order-other-family-only" && device.GetContext().settleFamily == device.GetContext().queueFamily) { + std::cout << "skipped, no other queue family to settle imports on\n"; + return 77; + } + return settleWriteOrderTests(device) ? 0 : 77; + } if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; From 894d9f81d43eef187d3c62a9b0119702442ad586 Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Tue, 20 Oct 2026 16:33:21 +0200 Subject: [PATCH 214/318] docs(agc): describe deferred settle submission ordering --- docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/libSceAgcDriver.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 8676f9acff..77bf4fd9af 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -145,7 +145,7 @@ flowchart LR - Guest memory reaches the GPU through host imports (`VK_EXT_external_memory_host`) where the driver accepts them, and through copies otherwise ([`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). A registered range larger than the import window (64 MiB) is imported per window for buffer and fill accesses and whole for an address-based build. Live imports stay under a budget, with the least recently used import retired first (see TechnicalDebt, Host import windows and budget). - A guest protect or unmap that overlaps a plain host-pointer import retires the whole import before changing the guest range (`WaitForLeasesAndImports` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp)). Later accesses import surviving registered ranges again; imports never keep the pages across a cut pinned in a trimmed state. `GuestAllocations::Range::backingSerial` distinguishes the original pages from replacements: untouched pieces of a split keep their serial, newly mapped or protected pieces get a fresh one, and every span passed to `RequireUnpinned` (including the other mappings of remapped direct memory and the resident runs touched by a 2 MiB unmap or remap) gets a fresh serial when the mutation ends. - A draw binds its vertex and index ranges in place from a host import, except with NVIDIA's Windows driver, which reads a host import of guest memory far slower than a buffer it allocated (Astro Bot at Crash Site on an RTX 5070 Ti: 164 against 45 ms of draws per frame), so they are copied there (`DrawInputsInPlaceUsable`, `Context::drawInputsInPlace`). `APS5_DRAW_INPUTS_IN_PLACE=1` binds them in place and `APS5_NO_DRAW_INPUTS_IN_PLACE=1` copies them on any device; the second wins. -- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the settle queue, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. +- On Windows, a host import of private `MEM_WRITE_WATCH` memory (singly mapped direct memory) stays on `GetWriteWatch` when `ProbeImportWriteProtection` shows the driver reports nothing written after the second GPU read and a GPU write, and a CPU store is collected (`decideImportWatch`). The driver marks every page of such an import written, asynchronously, until a GPU use of it completes (a read on the settle queue ends it even while main-queue work that used the import first is still running, and the first use of an earlier import of the same pages after that read marks nothing), so `settleImport` in [`GuestBufferMemory.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) copies 4 bytes out of the new import on a second queue of the graphics family, or on a queue of another copy-capable family when the graphics family has only one (`Context::settleQueue`, own pool and mutex) inside `ImportWatched`'s window and waits for it, for imports of 1 MiB or more; the main queue can't be used there because `importAllocation` holds the imports lock and `GpuMutex` comes before it in the lock order. A smaller import is compared when it is made, so a title that maps, imports and unmaps a small buffer around every submission does not block its mapping thread. Its 4-byte read is queued instead: the next import lookup that holds `GpuMutex` while the imports match the registry (`settleUnwatchedImports`) records the queued reads into one batch on the main queue, with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU, and once the batch's fence has signalled a later lookup returns the import's whole 64 KiB blocks to the write watch (`GuestMemory::Rewatch` drops their marks and stamps every block as a CPU store), unless a live storage image overlaps them. Such an import stays compared: an image that took its generation before the stamp would take the stamp for a CPU store over the results it produces after it, and drop them. A settled import whose range had already left the watch returns the same way, and an import retired while its read is in flight is kept with that batch and freed once the batch has completed, without waiting for it; a guest unmap of its memory waits for the batch instead. `APS5_NO_DEFERRED_SETTLES` (any value) keeps every import under 1 MiB compared. Imports through a section alias keep tracking on the guest views, and every other Windows host import is compared. A device without such a queue keeps the comparisons. On Linux the settle queue is made when the device imports dma-buf memory, and every dma-buf import of 1 MiB or more is settled right after it is made, for another reason: the kernel maps an import's pages for the GPU at the first submission that uses it, which took about 2 s for a 3 GiB import on RADV and held up the title's frame. The import's buffer is shared with the settle queue's family instead of aliased, since a sparse import is bound to many memory objects. ### Log output diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 6de88a50ab..2116ab2ea3 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -114,7 +114,8 @@ - [`APS5_SKIP_REFUSED_DRAWS`](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawCapture.cpp) (libSceAgcDriver) drops a draw whose graphics stage the recompiler refuses instead of failing the queue, so the frame lacks that draw, and the stage is compiled and refused again at each such draw. Compute refusals still fail. - [Pending storage-image results over memory outside the write watch](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp) (libSceAgcDriver) leave to the CPU the 64 KiB blocks whose bytes differ from the ones saved at the upload, the fill, the image copy, the previous store or the moment their range left the watch (`Unwatch`, or a host import that does not stay watched), and the blocks the watch saw the CPU write before that moment. A store made on the GPU saves its bytes through a host-visible copy that becomes the saved bytes when its batch completes; a store or a refresh that needs them earlier waits for that batch. A unit without a write generation saves the guest bytes when results land in it outside the watch. One whose range leaves the watch while its results are pending saves its bytes through the unwatch hook, which also runs when a failed collect takes a range out of the watch. A range that leaves the watch without the hook running over it (an import fallback that unwatched without saving) leaves its pending results without saved bytes; storing such results throws. A unit whose results were stored while its blocks were watched keeps only a generation; when its blocks leave the watch while nothing is pending, the hook saves nothing, and results that land in it afterwards have neither saved bytes nor a watch, so storing them throws too. - Windows host-pointer imports of the guest mapping itself are compared instead of write-watched, except writable imports of private `MEM_WRITE_WATCH` memory when the import probe keeps the write watch and a settle queue exists: those of 1 MiB or more stay watched, and smaller ones return to the watch once their settle has completed, unless a storage image overlaps them (see Pages under an import window): a driver can mark imported pages dirty after the synchronous import window. Shared memory imported through a separate writable alias retains protection-based tracking on the guest views; driver access to the import alias does not disarm those views. [GuestMemory](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. Excluded blocks keep their write-watch marks as a hint only (`UntrackedHint`): a storage image or a sampled texture snapshot over them compares its bytes again after a mark in its range, a driver store, a mapping change or a recorded GPU write instead of at every collect epoch or lookup, and a mark the driver makes without a CPU store costs one comparison. This preserves CPU edits through byte comparisons but limits snapshot reuse on excluded blocks to draws of one collect epoch and disables unit shadows for them; restoring tracking on import retirement without replacing the backing remains unimplemented. -- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) (libSceAgcDriver) whose write protection was resolved while a host import that leaves its range watched was made are compared with the bytes they held before the import and count as stored over only when those differ. A CPU store of the bytes already there within the window cannot be told apart from an import that resolves the write protection without storing, as the NVIDIA driver (610.57.04) does for every imported page, so such a store does not win over pending results. Where host-pointer and dma-buf imports differ in whether they stay watched, a failed host-pointer import and the dma-buf import after it are two windows, and the pages the failed one resolved count as stored over. On Windows, the NVIDIA driver (617.42) marks every page of a host import of private `MEM_WRITE_WATCH` memory written, asynchronously, until a GPU use of the import completes; a 4-byte read of it on the settle queue ends that even while main-queue work that used the import first is still running, and an earlier import of the same pages whose first use runs after that read marks none of them (`agc_import_settle_first_use`, `agc_import_settle_earlier_import`). Such an import stays watched only after a 4-byte read of it on the settle queue has completed inside the window. Each one waits for that read, which keeps its window open for one round trip on the settle queue, so a same-value CPU store from another thread in that time counts as unchanged too. An import made where no settle queue exists is compared instead. An import of under 1 MiB is not settled inside its window, because the settle blocks the importing thread and a title that imports a 64 KiB buffer around every submission lost about 19 % of its frame rate to that wait (Dripl1x, #6060). It is compared until its read, submitted in a batch by a later import lookup under `GpuMutex` while the imports match the registry, has completed; a later lookup then returns its whole blocks to the write watch, each stamped as a CPU store, so whatever was derived from them while they were compared reads as changed once. Its blocks stay compared while storage results are pending over them, and those of an import retired before that stay compared. +- [Pages under an import window](../../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) whose protection an import resolves count as stored only when their bytes change. A same-value CPU store during that window cannot be distinguished from driver activity and does not win over pending results. On Windows, NVIDIA 617.42 marks every private MEM_WRITE_WATCH page after import until a GPU use completes. The driver behavior was measured on this driver; other drivers are unverified. Imports of at least 1 MiB wait for a four-byte read on the separate settle queue inside the window. Smaller imports stay compared until a deferred read completes; a later lookup returns whole blocks to write watching with a new CPU-store stamp. Blocks overlapping a live storage image, blocks of an import retired before rewatching, and imports without a settle queue stay compared. Shared-memory imports retain their separate-alias tracking. +- [Deferred host-import reads](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) use the main queue with barriers ordering earlier and later writes around the reads. Submission does not wait on the CPU. The latency cost of placing batches behind main-queue work is unmeasured. The first-use test now checks that earlier main-queue reads finish before the import returns to write watching; the original two-queue canary measured driver marking while those reads were still in flight. The earlier-import canary still checks later use of another import of the same pages. - [Settled dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a dma-buf import of 1 MiB or more is read by a 4-byte copy on the settle queue right after it is made (`settleImport`), so the kernel maps its pages for the GPU there and not in the first submission that uses it (about 2 s for a 3 GiB import on RADV). The import's buffer is shared with the settle queue's family. An import made at map time settles on the guest thread that maps the range, inside its map call, so that thread waits while the kernel maps the pages: 0.47 to 2.2 s for the 3 GiB range of Hades (PPSA03355) and 0.49 to 4.3 s for the 4.5 GiB range of ASTRO BOT (PPSA21567) on RADV. Both titles map these ranges at start-up; a title that maps large ranges during gameplay stalls its mapping thread for as long. An import made from a submission settles under the GPU mutex, so its frame still pays for it; a smaller import, and every import on a device with no settle queue (one without a copy-capable queue besides the main one), pays in its first submission. A range imported through a host pointer, as lavapipe imports shared memory, is not settled - [Sparse dma-buf imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): on Linux a shared-backed range larger than one 64 MiB udmabuf buffer is imported as one sparse buffer bound to the 64 MiB buffers, on a queue of its own when the device has a second family that binds sparse memory. The path is decided before anything is imported, and a failure after that throws. A range whose offset in the backing file is not a multiple of 64 KiB starts its sparse buffer at the 64 KiB block below it (`HostImport::lead`). A range takes the single udmabuf import instead, which udmabuf's `size_limit_mb` refuses above 64 MiB by default so the range is copied, when the buffer's sparse block size is larger than 64 KiB (it is 64 KiB on RADV), when the sparse buffer does not accept every memory type a chunk import can have, when the backing file ends inside its last chunk, or when it is imported at map time on a device with no second family (the bind then needs the GPU mutex); the NVIDIA proprietary driver imports no dma-buf memory - [Unnormalized S# coordinates](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. From e6d7e36dcf3baa034536e8d9e0ed8fa31348e213 Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Fri, 23 Oct 2026 13:10:52 +0200 Subject: [PATCH 215/318] fix(agc): bound deferred settle retirement waits --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../Graphics/src/GuestBufferMemory.cpp | 2 +- .../prx/libSceAgcDriver/tests/Recorder.cpp | 109 ++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e966e93c15..2647cdce37 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1491,8 +1491,10 @@ if(BUILD_TESTING) add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only) add_test(NAME agc_import_settle_write_order COMMAND agc_driver_recorder_tests --settle-write-order-only) add_test(NAME agc_import_settle_write_order_other_family COMMAND agc_driver_recorder_tests --settle-write-order-other-family-only) - set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family PROPERTIES + add_test(NAME agc_import_settle_wait_failure COMMAND agc_driver_recorder_tests --settle-wait-failure-only) + set_tests_properties(agc_import_settle_write_order agc_import_settle_write_order_other_family agc_import_settle_wait_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 + FAIL_REGULAR_EXPRESSION "SYNC-HAZARD-|VUID-" ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only) add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index 45ea9b5901..d2e39b540e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -293,7 +293,7 @@ HostImports& Imports() { #ifdef _WIN32 void waitSettleBatch(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { - if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, UINT64_MAX), "vkWaitForFences (settle batch)"); + if (batch.busy && settle.waitForFences != nullptr) Check(settle.waitForFences(device, 1, &batch.fence, VK_TRUE, 60'000'000'000ull), "vkWaitForFences (settle batch)"); } bool settleBatchRunning(const ImportSettle& settle, VkDevice device, const SettleBatch& batch) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 667b46c27f..cbffabd1a4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2131,6 +2131,114 @@ bool settleWriteOrderTests(const Device& device) { return true; } +struct SettleWaitFailure { + PFN_vkGetDeviceProcAddr resolve; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkResult result = VK_SUCCESS; + std::uint64_t timeout = 0; + unsigned waits = 0; + unsigned destroys = 0; + unsigned frees = 0; +}; + +SettleWaitFailure* settleWaitFailure = nullptr; + +VKAPI_ATTR VkResult VKAPI_CALL settleWaitForFences(VkDevice device, std::uint32_t count, const VkFence* fences, VkBool32 all, std::uint64_t timeout) { + auto& failure = *settleWaitFailure; + if (failure.result != VK_SUCCESS) { + failure.timeout = timeout; + ++failure.waits; + return failure.result; + } + return reinterpret_cast(failure.resolve(device, "vkWaitForFences"))(device, count, fences, all, timeout); +} + +VKAPI_ATTR void VKAPI_CALL settleDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) { + auto& failure = *settleWaitFailure; + if (buffer == failure.buffer) ++failure.destroys; + reinterpret_cast(failure.resolve(device, "vkDestroyBuffer"))(device, buffer, allocator); +} + +VKAPI_ATTR void VKAPI_CALL settleFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) { + auto& failure = *settleWaitFailure; + if (memory == failure.memory) ++failure.frees; + reinterpret_cast(failure.resolve(device, "vkFreeMemory"))(device, memory, allocator); +} + +PFN_vkVoidFunction VKAPI_CALL settleWaitDeviceProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkWaitForFences") == 0) return reinterpret_cast(settleWaitForFences); + if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast(settleDestroyBuffer); + if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast(settleFreeMemory); + return settleWaitFailure->resolve(device, name); +} + +bool settleWaitFailureTests(const Device& device) { + using namespace AgcDriver::GuestMemory; + auto context = device.GetContext(); + constexpr std::size_t unit = 65536; + if (context.hostImportAlignment == 0 || context.hostImportAlignment > unit || context.settleQueue == VK_NULL_HANDLE || !WriteWatched()) return false; + std::lock_guard gpu(GpuMutex()); + PrepareImportWatch(context); + if (!settledImportsWatchPrivate(context)) return false; + ClearHostImports(context.device); + SettleWaitFailure failure{context.deviceProc}; + settleWaitFailure = &failure; + context.deviceProc = settleWaitDeviceProc; + context.functions = nullptr; + void* block = AllocateWatched(unit, unit); + Require(block != nullptr, "no watched block for settle wait failure"); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, unit, true, true, true); + } + struct Release { + Context& context; + SettleWaitFailure& failure; + void* block; + ~Release() { + failure.result = VK_SUCCESS; + ClearHostImports(context.device); + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + ReleaseWatched(block, unit); + settleWaitFailure = nullptr; + } + } release{context, failure, block}; + const auto address = reinterpret_cast(block); + const auto* import = HostImportFor(context, address, unit); + Require(import != nullptr && import->unwatched, "wait failure import was not initially compared"); + failure.buffer = import->buffer; + failure.memory = import->memory; + const auto before = HostImportSettles().submitted; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (HostImportSettles().submitted == before && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + HostImportFor(context, address, unit); + } + Require(HostImportSettles().submitted > before, "wait failure did not submit a settle"); + Check(context.Function("vkDeviceWaitIdle")(context.device), "vkDeviceWaitIdle before settle failure"); + for (const auto result : {VK_TIMEOUT, VK_ERROR_DEVICE_LOST}) { + failure.result = result; + const auto waits = failure.waits; + std::string error; + try { + ClearHostImports(context.device); + } catch (const std::runtime_error& caught) { + error = caught.what(); + } + failure.result = VK_SUCCESS; + Require(failure.waits == waits + 1 && failure.timeout > 0 && failure.timeout <= 60'000'000'000ull, "settle retirement did not use a bounded fence wait"); + Require(error == "AGC graphics: vkWaitForFences (settle batch): Vulkan result " + std::to_string(result), "settle retirement did not propagate its Vulkan failure"); + Require(failure.destroys == 0 && failure.frees == 0 && HostImportCovers(context, address, unit), "failed settle wait released its host import"); + } + ClearHostImports(context.device); + Require(failure.destroys == 1 && failure.frees == 1, "successful settle retirement did not release the retained import exactly once"); + std::cout << "bounded settle wait preserves imports on timeout and device loss; successful retry releases once\n"; + return true; +} bool settleFirstUseTests(const Device& device) { using namespace AgcDriver::GuestMemory; @@ -10591,6 +10699,7 @@ int main(int argc, char** argv) { } return settleWriteOrderTests(device) ? 0 : 77; } + if (argc == 2 && std::string_view(argv[1]) == "--settle-wait-failure-only") return settleWaitFailureTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-first-use-only") return settleFirstUseTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-earlier-import-only") return settleEarlierImportTests(device) ? 0 : 77; if (argc == 2 && std::string_view(argv[1]) == "--settle-retire-only") return settleRetireTests(device) ? 0 : 77; From b996e31d8c11956df72031e42d286e4682ebe1fe Mon Sep 17 00:00:00 2001 From: eliasbruno124-dev Date: Tue, 20 Oct 2026 20:29:11 +0200 Subject: [PATCH 216/318] perf(agc): reclaim idle device buffers under storage cache pressure --- .../Graphics/include/BufferPool.hpp | 5 + .../Graphics/src/BufferPool.cpp | 50 +++++++ .../Graphics/src/ShaderResources.cpp | 30 ++-- .../prx/libSceAgcDriver/tests/BufferPool.cpp | 140 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 5 files changed, 216 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 5dcf54c727..495f80ee0a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -73,6 +73,7 @@ public: std::size_t RetainedImages(); std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); void Put(const BufferAllocation& allocation) noexcept; + VkDeviceSize ReclaimIdleDevice(VkDeviceSize bytes, std::chrono::steady_clock::time_point now); static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom); static VkDeviceSize SlabBlockBytes(std::size_t capacity); SlabSlot TakeSlot(const Context& context, std::uint32_t memoryType, std::size_t capacity, bool addressable); @@ -168,6 +169,10 @@ private: std::uint64_t imageEvictions = 0; std::list imageOrder; std::uint64_t clock = 0; + std::optional idleRead; + std::uint64_t idleClock = 0; + std::uint64_t reclaimBefore = 0; + std::uint64_t scannedIdleClock = 0; AgcDriver::Mutex slabMutex; std::unordered_map slabs; std::unordered_map> slabBlocks; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 128970a898..47d1c1af23 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -454,6 +454,56 @@ bool BufferPool::evictOldestImage(std::vector& evicted) { return true; } +VkDeviceSize BufferPool::ReclaimIdleDevice(VkDeviceSize bytes, std::chrono::steady_clock::time_point now) { + if (bytes == 0) return 0; + const auto heaps = std::min(memoryProperties.memoryHeapCount, VK_MAX_MEMORY_HEAPS); + if (std::count_if(memoryProperties.memoryHeaps, memoryProperties.memoryHeaps + heaps, [](const auto& heap) { return (heap.flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0; }) != 1) return 0; + std::vector evicted; + { + std::lock_guard lock(mutex); + if (!idleRead) { + idleRead = now; + idleClock = clock; + return 0; + } + if (now < *idleRead) return 0; + if (now - *idleRead >= std::chrono::seconds(1)) { + reclaimBefore = idleClock; + idleClock = clock; + idleRead = now; + } + if (reclaimBefore <= scannedIdleClock) return 0; + VkDeviceSize selected = 0; + while (selected < bytes) { + auto oldest = deviceTier.free.end(); + for (auto it = deviceTier.free.begin(); it != deviceTier.free.end(); ++it) { + if (it->second.empty()) continue; + const auto& slot = it->second.front(); + if (slot.allocation.buffer == VK_NULL_HANDLE || slot.allocation.slab || slot.lastUse > reclaimBefore) continue; + if (oldest == deviceTier.free.end() || slot.lastUse < oldest->second.front().lastUse) oldest = it; + } + if (oldest == deviceTier.free.end()) { + scannedIdleClock = reclaimBefore; + break; + } + if (evicted.empty()) evicted.reserve(deviceTier.slots); + const auto allocation = oldest->second.front().allocation; + evicted.push_back(allocation); + selected += allocation.allocationBytes; + deviceTier.retainedBytes -= allocation.allocationBytes; + oldest->second.pop_front(); + if (oldest->second.empty()) deviceTier.free.erase(oldest); + --deviceTier.slots; + ++deviceTier.evictions; + } + } + VkDeviceSize released = 0; + for (const auto& allocation : evicted) released += destroy(allocation); + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (profile && released != 0) AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] idle device reclaim: %llu bytes released for %llu bytes of storage-cache pressure\n", static_cast(released), static_cast(bytes)); + return released; +} + std::size_t BufferPool::MaxSlots() { // APS5_BUFFER_POOL_SLOTS= bounds the retained allocations; 64 is the capacity the pool had // before least-recently-used retention, for A/B runs. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 863d6cd9cf..a7177e41f7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -9,6 +9,7 @@ #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" @@ -572,7 +573,7 @@ std::uint64_t forcedCacheBudget() { return forced; } -std::uint64_t storageBudget(const Context& context) { +std::uint64_t storageBudget(const Context& context, std::uint64_t required = 0) { const auto forced = forcedCacheBudget(); if (forced != 0) return forced; if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return TextureCacheBudget(context.memory); @@ -581,17 +582,26 @@ std::uint64_t storageBudget(const Context& context) { VkPhysicalDevice device{VK_NULL_HANDLE}; std::chrono::steady_clock::time_point read{}; std::uint64_t budget{0}; + std::uint64_t ceiling{0}; } cached; const auto now = std::chrono::steady_clock::now(); std::lock_guard lock(cached.mutex); - if (cached.device == context.physical && now - cached.read < std::chrono::seconds(1)) return cached.budget; - VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; - VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; - properties.pNext = &reported; - context.memoryProperties2(context.physical, &properties); - cached.device = context.physical; - cached.read = now; - cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes()); + const auto read = [&] { + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2}; + properties.pNext = &reported; + context.memoryProperties2(context.physical, &properties); + cached.device = context.physical; + cached.read = now; + cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes()); + cached.ceiling = cached.budget; + if (const auto heap = largestDeviceLocalHeap(context.memory); heap && reported.heapBudget[*heap] != 0) { + cached.ceiling = std::max(512ull << 20u, std::min(TextureCacheBudget(context.memory), reported.heapBudget[*heap] / 4u)); + } + }; + if (cached.device != context.physical || now - cached.read >= std::chrono::seconds(1)) read(); + const auto needed = std::min(required, cached.ceiling); + if (needed > cached.budget && context.bufferPool != nullptr && context.bufferPool->ReclaimIdleDevice(needed - cached.budget, now) != 0) read(); return cached.budget; } @@ -1182,7 +1192,7 @@ std::shared_ptr cachedStorageTexture(const Context& context, std static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr; if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture); entry.accounted = heldBytes(*entry.texture); - const auto budget = storageBudget(context); + const auto budget = storageBudget(context, cache.bytes + entry.accounted); while (!cache.entries.empty() && cache.bytes + entry.accounted > budget) evicted.push_back(evictStorage(cache, std::prev(cache.entries.end()))); cache.bytes += entry.accounted; texture = entry.texture; diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index abdf8f0166..9547cfb6c2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -832,6 +832,103 @@ void AllocationFailureReportsHeapBudgets() { Expect(diagnosticBudgetQueries == 0, "missing-extension fallback attempted a memory budget query"); } +Context reclaimContext() { + auto context = mockContext(); + context.memory.memoryHeapCount = 2; + context.memory.memoryHeaps[0] = {4096 * MiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT}; + context.memory.memoryHeaps[1] = {8192 * MiB, 0}; + context.memory.memoryTypes[1].heapIndex = 1; + return context; +} + +void IdleDeviceReclaim() { + mock = MockDevice{}; + { + auto context = reclaimContext(); + VkBuffer cold; + VkBuffer nextCold; + VkBuffer hot; + VkBuffer host; + VkBuffer small; + { DeviceBuffer buffer(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); cold = buffer.Handle(); } + { DeviceBuffer buffer(context, 3 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); nextCold = buffer.Handle(); } + { DeviceBuffer buffer(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); hot = buffer.Handle(); } + { Buffer buffer(context, 8 * MiB, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); host = buffer.Handle(); } + { Buffer buffer(context, 1024, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); small = buffer.Handle(); } + DeviceBuffer active(context, 16 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + auto& pool = *context.bufferPool; + const auto start = std::chrono::steady_clock::time_point{std::chrono::seconds(1)}; + Expect(pool.ReclaimIdleDevice(MiB, start) == 0, "the first pressure observation reclaimed a buffer without an idle interval"); + Expect(pool.ReclaimIdleDevice(MiB, start + std::chrono::milliseconds(999)) == 0, "a partial idle interval reclaimed a buffer"); + { + DeviceBuffer reused(context, 4 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Expect(reused.Handle() == hot, "the warm device buffer was not reused"); + } + const auto next = start + std::chrono::seconds(1); + SetAllocationFailureCountdown(0); + bool threw = false; + try { + pool.ReclaimIdleDevice(MiB, next); + } catch (const std::bad_alloc&) { + threw = true; + } + SetAllocationFailureCountdown(-1); + Expect(threw && mock.liveBuffers.contains(cold) && mock.liveBuffers.contains(nextCold), "a failed reclaim collection lost ownership of an allocation"); + Expect(pool.ReclaimIdleDevice(MiB, next) == 2 * MiB, "reclaim did not release exactly the oldest complete idle allocation"); + Expect(!mock.liveBuffers.contains(cold) && mock.liveBuffers.contains(nextCold), "bounded reclaim released the wrong allocation or exceeded its request"); + Expect(pool.ReclaimIdleDevice(0, next) == 0, "zero pressure released a buffer"); + Expect(pool.ReclaimIdleDevice(32 * MiB, next) == 3 * MiB, "reclaim counted a hot, live, host or slab allocation as idle device memory"); + for (const auto buffer : {hot, host, small, active.Handle()}) Expect(mock.liveBuffers.contains(buffer), "reclaim destroyed a protected buffer"); + SetAllocationFailureCountdown(0); + const auto empty = pool.ReclaimIdleDevice(32 * MiB, next); + SetAllocationFailureCountdown(-1); + Expect(empty == 0, "an unchanged empty eligible set did not stay empty"); + Expect(pool.ReclaimIdleDevice(32 * MiB, start) == 0, "a backwards time observation reclaimed a buffer"); + Expect(pool.ReclaimIdleDevice(32 * MiB, next + std::chrono::seconds(1)) == 4 * MiB, "a device buffer did not become reclaimable after a complete idle interval"); + Expect(mock.liveBuffers.contains(active.Handle()), "reclaim touched an allocation still in use"); + Expect(pool.ReclaimIdleDevice(32 * MiB, next + std::chrono::seconds(2)) == 0, "an empty device tier reported reclaimed memory"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "idle device reclaim leaked an allocation"); +} + +void IdleReclaimPreservesWorkingSet() { + mock = MockDevice{}; + { + auto context = reclaimContext(); + const auto useWorkingSet = [&] { + std::vector> buffers; + for (unsigned i = 0; i < 4; ++i) buffers.push_back(std::make_unique(context, 96 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT)); + }; + useWorkingSet(); + const auto allocations = mock.allocations; + const auto start = std::chrono::steady_clock::time_point{std::chrono::seconds(1)}; + auto& pool = *context.bufferPool; + Expect(pool.ReclaimIdleDevice(1024 * MiB, start) == 0, "initial pressure discarded the working set"); + for (unsigned second = 1; second <= 4; ++second) { + useWorkingSet(); + Expect(pool.ReclaimIdleDevice(1024 * MiB, start + std::chrono::seconds(second)) == 0, "pressure discarded buffers reused during the observation interval"); + Expect(mock.allocations == allocations && mock.liveBytes == 384 * MiB, "a reusable working set larger than 128 MiB was recreated or shrunk"); + } + Expect(pool.ReclaimIdleDevice(1024 * MiB, start + std::chrono::seconds(5)) == 384 * MiB, "the former working set remained retained after becoming idle"); + Expect(mock.liveBytes == 0, "reclaim did not return the idle working set to the device"); + } + Expect(mock.liveBuffers.empty(), "working-set reclaim leaked a buffer"); + for (const auto heaps : {0u, 2u}) { + mock = MockDevice{}; + { + auto context = reclaimContext(); + context.memory.memoryHeapCount = heaps; + context.memory.memoryHeaps[1].flags = VK_MEMORY_HEAP_DEVICE_LOCAL_BIT; + { DeviceBuffer buffer(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); } + auto& pool = *context.bufferPool; + const auto start = std::chrono::steady_clock::time_point{std::chrono::seconds(1)}; + Expect(pool.ReclaimIdleDevice(4 * MiB, start) == 0 && pool.ReclaimIdleDevice(4 * MiB, start + std::chrono::seconds(1)) == 0, "reclaim assumed a device heap without unambiguous ownership"); + Expect(mock.liveBytes == 2 * MiB, "reclaim released memory from an unknown or different local heap"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "unsupported-heap reclaim leaked an allocation"); + } +} + void SuccessfulAllocationKeepsPool() { mock = MockDevice{}; { @@ -1001,10 +1098,50 @@ void ImageMemoryReuse() { Expect(mock.frees == 35 && context.bufferPool->RetainedImages() == 1, "the pool retains image memory again after a trim"); } +void IdleReclaimPreservesImages() { + mock = MockDevice{}; + { + auto context = reclaimContext(); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = 4 * MiB; + allocation.memoryTypeIndex = 0; + std::uint32_t type = 99; + const auto image = AllocateImageMemory(context, allocation, 1u, "test idle image", &type); + ReleaseImageMemory(context, image, 4 * MiB, type); + VkBuffer buffer; + { DeviceBuffer idle(context, 2 * MiB, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); buffer = idle.Handle(); } + auto& pool = *context.bufferPool; + const auto start = std::chrono::steady_clock::time_point{std::chrono::seconds(1)}; + Expect(pool.RetainedImages() == 1 && mock.liveBytes == 6 * MiB, "the mixed pool fixture was not retained"); + Expect(pool.ReclaimIdleDevice(MiB, start) == 0, "initial pressure reclaimed mixed pool memory"); + const auto next = start + std::chrono::seconds(1); + const auto released = pool.ReclaimIdleDevice(MiB, next); + Expect(released == 2 * MiB, "idle buffer reclaim selected image memory instead of the oldest buffer"); + if (released != 2 * MiB) return; + Expect(!mock.liveBuffers.contains(buffer) && mock.liveBytes == 4 * MiB && pool.RetainedImages() == 1, "buffer reclaim changed image ownership or accounting"); + Expect(pool.ReclaimIdleDevice(32 * MiB, next) == 0, "an image-only pool was reclaimed as idle buffers"); + SetAllocationFailureCountdown(0); + const auto empty = pool.ReclaimIdleDevice(32 * MiB, next); + SetAllocationFailureCountdown(-1); + Expect(empty == 0, "a repeated image-only scan allocated or reclaimed memory"); + const auto allocations = mock.allocations; + const auto reused = AllocateImageMemory(context, allocation, 1u, "test idle image", &type); + Expect(reused == image && mock.allocations == allocations && pool.RetainedImages() == 0, "the retained image was not reusable after buffer reclaim"); + ReleaseImageMemory(context, reused, 4 * MiB, type); + Expect(pool.RetainedImages() == 1 && mock.liveBytes == 4 * MiB, "the reused image was not retained exactly once"); + } + Expect(mock.liveBytes == 0 && mock.liveBuffers.empty() && mock.frees == 2, "mixed pool cleanup leaked or freed an allocation twice"); +} + + int main(int argc, char** argv) { try { if (argc == 2 && std::string_view(argv[1]) == "--small-device-retention") { SmallDeviceRetention(); + if (std::getenv("APS5_STAGING_POOL_MIB") == nullptr) { + IdleDeviceReclaim(); + IdleReclaimPreservesWorkingSet(); + } return failures == 0 ? 0 : 1; } if (argc == 2) { @@ -1040,6 +1177,9 @@ int main(int argc, char** argv) { AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true); AllocationFailureReportsHeapBudgets(); SuccessfulAllocationKeepsPool(); + IdleDeviceReclaim(); + IdleReclaimPreservesWorkingSet(); + IdleReclaimPreservesImages(); PartialSlabDoesNotRetry(); CacheReclaimRecovery(); ImageCacheReclaim(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 2116ab2ea3..67b3d2b74f 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -102,6 +102,7 @@ - [Covered-centroid raster state](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) accepts PA_SC_AA_CONFIG.COVERED_CENTROID_IS_CENTER only when the pixel program is skipped or SPI_PS_INPUT_ENA and SPI_PS_INPUT_ADDR have no active perspective or linear centroid input. Bit 29 follows [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json); [RADV](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/vulkan/radv_cmd_buffer.c) sets it on GFX10.3 and later. Active centroid interpolation and unspecified input registers still throw. Console covered-pixel centroid results were not measured. - [Compute LDS beyond the device's shared-memory limit](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) lives in a coherent storage buffer with one slice of `LDS bytes / 4 + 1` dwords, rounded up to an even count so that 64-bit atomics stay 8-byte aligned, per workgroup (the dword after the LDS is the atomic lock on a device without `shaderBufferInt64Atomics`), shared by all such dispatches and grown in 1 MiB steps up to `maxStorageBufferRange`; a dispatch whose slices do not fit throws. It is slower than shared memory and its cost was not measured. An indirect dispatch of such a program always reads its group counts on the CPU. Its results are pinned against the in-limit semantics in `agc_driver_lds_beyond_device_limit`, not measured on hardware. - [AGC buffer allocations](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) retry host/device out-of-memory once after reclaiming idle pool buffers and fully unused slab blocks. Live buffers and blocks with live slots are retained. Live images and other retained resources have no global memory bound; allocation failure still throws when reclaim cannot satisfy the request. +- [Storage-cache pressure and idle device buffers](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp): the one-second idle observation interval is a retention choice, not a console rule; buffers reused less often can be recreated under pressure. Before a storage-cache eviction, independently allocated device-tier buffers unused throughout that interval can be released, oldest first, for the cache's shortfall below its existing ceiling. Live buffers, host tiers and slab allocations are not reclaimed this way. Reclamation requires exactly one device-local heap because retained buffers do not record their heap. The heap budget is read again after an actual release; a delayed driver report can still cause an eviction. This is not a shared budget for all retained GPU resources, and its effect on titles with intermittent large buffer use is unmeasured. - [Host placement after a device-memory refusal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp) (libSceAgcDriver): when the driver refuses a device-local allocation with `VK_ERROR_OUT_OF_DEVICE_MEMORY` after the pool's trim and the texture-cache release, device buffers and images (textures, storage textures, block, repacked, depth, depth plane and stencil copies, render, depth and multisampled targets and attachment proxies) are allocated in a host memory type their requirements allow. A host-placed device buffer leaves the pool on release. A host-placed image keeps that placement until it is destroyed and is never moved back to device memory, so every access to it crosses PCIe for its lifetime. The placements are counted in a rate-limited `[bufferpool]` line with the cumulative MiB, not the resident bytes. The 1x1 denormal clear probe still allocates device memory directly and throws on a refusal. - [Pipeline journal](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp) (libSceAgcDriver): the journal is append-only and never compacted, so a module a later run no longer builds pipelines from stays in the file until it is deleted, and the whole file is read into memory while the replay runs (its modules reference that buffer until the last replayed pipeline was built). Only the first `APS5_PIPELINE_JOURNAL_ENTRIES` pipelines recorded are replayed (a record is never rewritten, so a pipeline past the bound stays past it), and the pipeline store evicts the least recently used entry past `APS5_PIPELINE_STORE_ENTRIES` (1024 each): a title with more configurations rebuilds the rest at the draw as before. A compute pipeline is replayed over the set layout `DescribeSetLayouts` derives from the result's bindings, which a dispatch that finds it resident checks against its `ShaderResources` layout only under `APS5_VERIFY_SET_LAYOUTS=1`. Recording and replay are best effort: a record whose write fails is lost, an entry whose build fails is skipped, and both are counted in the `[pipeline-journal]` line rather than stopping the run. A replayed pipeline that the title never asks for again costs its build time and its driver memory (its optimized relink is deferred to its first draw, so a run whose first frames bind many replayed pipelines queues their relinks then); nothing ages recipes out of the file. Two runs sharing the file take turns under its lock file, but each only knows the modules it loaded or wrote itself, so a module the other run appended meanwhile may be appended again (the loader keeps the first copy). Records a run appends after another run's load are replayed by that run only after its next start. The file header's format number is the only guard against a change of the recipe codec, `CanonicalPipelineState` or the store key that keeps the structures' sizes (the recompiler's source version does not cover the driver's sources). - [Graphics pipeline libraries](../../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineLibrary.cpp) (libSceAgcDriver): a pipeline is fast-linked at the draw and relinked with `VK_PIPELINE_CREATE_LINK_TIME_OPTIMIZATION_BIT_EXT` on a background thread, and binds the optimized one from the next draw on. Draws recorded before that use the fast link, whose vertex stage keeps the outputs the fragment stage does not read: on RADV (Radeon 660M, Mesa 26.2.4) 346 of 483 Astro's Playroom vertex shaders paired with a fragment stage that reads none of them used more VGPRs (median 8 more, up to 2.5 times), and on an RTX 4090 13 of 15 PPSA36082 vertex shaders up to 68 instead of 38 (#2242). The optimized link compiles to the same code as a whole pipeline From e6daabdad83513868cde737087f6dc101154c057 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:42:29 +0200 Subject: [PATCH 217/318] test(libkernel): keep descriptor 0 open when guest_kqueue starts with standard input closed When the process starts with descriptor 0 closed, open returns 0, dup2(0, 0) succeeds and closing the temporary descriptor then closes descriptor 0 again, so the standard input case returned early at its EBADF branch. Skip the dup2 and the close when the temporary descriptor already is 0, on both platform branches, and check the replacement for an initially open and an initially closed standard input. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestKqueue.cpp | 37 +++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index d7c73cad1e..fd0acdad4e 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -1121,15 +1121,43 @@ static void Timeout() { static void ReplaceStandardInputWithNull() { #ifdef _WIN32 const int null = _open("NUL", _O_RDONLY); - Require(null >= 0 && _dup2(null, 0) == 0); - Require(_close(null) == 0); + Require(null >= 0); + if (null != 0) { + Require(_dup2(null, 0) == 0); + Require(_close(null) == 0); + } #else const int null = ::open("/dev/null", O_RDONLY); - Require(null >= 0 && ::dup2(null, 0) == 0); - Require(::close(null) == 0); + Require(null >= 0); + if (null != 0) { + Require(::dup2(null, 0) == 0); + Require(::close(null) == 0); + } #endif } +static bool StandardInputAtEnd() { + char byte = 0; +#ifdef _WIN32 + return _read(0, &byte, 1) == 0; +#else + return ::read(0, &byte, 1) == 0; +#endif +} + +static void StandardInputReplacement() { + ReplaceStandardInputWithNull(); + Require(StandardInputAtEnd()); +#ifdef _WIN32 + Require(_close(0) == 0); +#else + Require(::close(0) == 0); +#endif + ReplaceStandardInputWithNull(); + Require(StandardInputAtEnd()); +} + + static void StandardInput() { ReplaceStandardInputWithNull(); const int copy = dup_nid_postfix(0); @@ -1153,6 +1181,7 @@ static void StandardInput() { } int main() { + StandardInputReplacement(); StandardInput(); CreateAndClose(); Timers(); From 07d3d6447acd38499d2bb619f016576baff71803 Mon Sep 17 00:00:00 2001 From: QuentinRemond Date: Thu, 15 Oct 2026 20:39:32 +0200 Subject: [PATCH 218/318] feat(libkernel_sys): implement fpathconf and _fpathconf Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel_sys/Export.cpp | 71 ++++++++++++++++---------- core/libs/tests/GuestKernelSys.cpp | 43 ++++++++++++++++ docs/dev/debt/libkernel_sys.md | 2 +- 3 files changed, 87 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 03571bcf29..880b594c50 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -29,6 +29,8 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/KernelErrors.hpp" extern "C" int APS5_VABI getpid_nid_postfix(void); #ifdef _WIN32 #ifndef NOMINMAX @@ -128,6 +130,37 @@ std::int64_t Fail(int error) { return -1; } +std::int64_t PathConfigurationValue(int name, const char* function) { + switch (name) { + case PcLinkMax: return 32767; + case PcNameMax: return static_cast(GuestNameMax); + case PcPathMax: return static_cast(GuestPathMax); + case PcPipeBuf: return 512; + case PcChownRestricted: return 1; + case PcNoTrunc: return 1; + case PcFileSizeBits: return 64; + case PcRecMaxXferSize: return -1; + case PcSymlinkMax: return static_cast(GuestPathMax); + case PcPrioIo: return 0; + case PcSyncIo: return 0; + case PcMaxCanon: + case PcMaxInput: + case PcVdisable: + case PcAllocSizeMin: + case PcRecIncrXferSize: + case PcRecMinXferSize: + case PcRecXferAlign: + case PcMinHoleSize: + case PcAsyncIo: + case PcAclExtended: + case PcAclPathMax: + case PcMacPresent: + case PcAclNfs4: + throw std::runtime_error(std::string(function) + ": name " + std::to_string(name) + " depends on the console filesystem or kernel configuration"); + default: return Fail(GuestEinval); + } +} + int GuestError(const std::error_code& error) { const auto condition = error.default_error_condition(); if (condition == std::errc::resource_unavailable_try_again) return GuestEagain; @@ -335,34 +368,16 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { const auto status = std::filesystem::status(native, error); if (error) return Fail(error == std::errc::no_such_file_or_directory && HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestError(error)); if (!std::filesystem::exists(status)) return Fail(HasNonDirectoryPrefix(native) ? GuestEnotdir : GuestEnoent); - switch (name) { - case PcLinkMax: return 32767; - case PcNameMax: return static_cast(GuestNameMax); - case PcPathMax: return static_cast(GuestPathMax); - case PcPipeBuf: return 512; - case PcChownRestricted: return 1; - case PcNoTrunc: return 1; - case PcFileSizeBits: return 64; - case PcRecMaxXferSize: return -1; - case PcSymlinkMax: return static_cast(GuestPathMax); - case PcPrioIo: return 0; - case PcSyncIo: return 0; - case PcMaxCanon: - case PcMaxInput: - case PcVdisable: - case PcAllocSizeMin: - case PcRecIncrXferSize: - case PcRecMinXferSize: - case PcRecXferAlign: - case PcMinHoleSize: - case PcAsyncIo: - case PcAclExtended: - case PcAclPathMax: - case PcMacPresent: - case PcAclNfs4: - throw std::runtime_error("pathconf: name " + std::to_string(name) + " depends on the console filesystem or kernel configuration"); - default: return Fail(GuestEinval); - } + return PathConfigurationValue(name, "pathconf"); +} + +std::int64_t APS5_VABI _fpathconf_nid_postfix(int fd, int name) { + if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return Fail(GuestEbadf); + return PathConfigurationValue(name, "fpathconf"); +} + +std::int64_t APS5_VABI fpathconf_nid_postfix(int fd, int name) { + return _fpathconf_nid_postfix(fd, name); } #ifdef _WIN32 diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a540b9b666..78bc8e9fdc 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -40,6 +40,8 @@ int APS5_VABI socket_nid_postfix(int, int, int); std::int64_t APS5_VABI readlink_nid_postfix(const char*, char*, std::size_t); int APS5_VABI lstat_nid_postfix(const char*, FileStat*); std::int64_t APS5_VABI pathconf_nid_postfix(const char*, int); +std::int64_t APS5_VABI fpathconf_nid_postfix(int, int); +std::int64_t APS5_VABI _fpathconf_nid_postfix(int, int); int APS5_VABI fchdir_nid_postfix(int); int APS5_VABI chdir_nid_postfix(const char*); int APS5_VABI chroot_nid_postfix(const char*); @@ -312,6 +314,38 @@ static void CheckProcessQueries() { Require(sceKernelGetProcessType(getpid_nid_postfix() + 1) == static_cast(0x80020003)); } +template +bool DescriptorPathconfThrows(F fn, int descriptor, int name) { + try { + fn(descriptor, name); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +template +void CheckDescriptorPathconf(F fn, int descriptor) { + Require(fn(descriptor, 1) == 32767); + Require(fn(descriptor, 4) == 255); + Require(fn(descriptor, 5) == 1024); + Require(fn(descriptor, 6) == 512); + Require(fn(descriptor, 7) == 1); + Require(fn(descriptor, 8) == 1); + Require(fn(descriptor, 12) == 64); + *__error_nid_postfix() = 0; + Require(fn(descriptor, 15) == -1 && Error() == 0); + Require(fn(descriptor, 18) == 1024); + Require(fn(descriptor, 54) == 0); + Require(fn(descriptor, 55) == 0); + for (const int name : {0, 11, 61, 62, 65, -1}) + Require(fn(descriptor, name) == -1 && Error() == 22); + for (const int name : {2, 3, 9, 10, 14, 16, 17, 21, 53, 59, 60, 63, 64}) + Require(DescriptorPathconfThrows(fn, descriptor, name)); + Require(fn(-1, 4) == -1 && Error() == 9); + Require(fn(descriptor + 1000, 4) == -1 && Error() == 9); +} + int main() { #ifdef _WIN32 auto hostDirectorySpelling = std::filesystem::current_path().native(); @@ -344,10 +378,19 @@ int main() { Require(fstat_nid_postfix(descriptor, &viaFstat) == 0 && _fstat_nid_postfix(descriptor, &viaUnderscore) == 0); Require(std::memcmp(&viaFstat, &viaUnderscore, sizeof(FileStat)) == 0 && viaFstat.st_size == 1); Require(fstat_nid_postfix(descriptor, nullptr) == -1 && Error() == 14); + CheckDescriptorPathconf(fpathconf_nid_postfix, descriptor); + CheckDescriptorPathconf(_fpathconf_nid_postfix, descriptor); Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, viaPublic) == -1 && Error() == 9); Require(fstat_nid_postfix(descriptor, &viaFstat) == -1 && Error() == 9); Require(fstat_nid_postfix(-1, &viaFstat) == -1 && Error() == 9); + Require(fpathconf_nid_postfix(descriptor, 4) == -1 && Error() == 9); + Require(_fpathconf_nid_postfix(descriptor, 4) == -1 && Error() == 9); + const int dirDescriptor = open_nid_postfix(root.generic_string().c_str(), 0, 0); + Require(dirDescriptor >= 0); + CheckDescriptorPathconf(fpathconf_nid_postfix, dirDescriptor); + CheckDescriptorPathconf(_fpathconf_nid_postfix, dirDescriptor); + Require(close_nid_postfix(dirDescriptor) == 0); char buffer[64]; for (const auto& component : {std::string("missing"), std::string("file.txt")}) { const auto prefix = root.generic_string() + "/" + component + "/../"; diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 49e8539bbc..085a67035e 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -15,7 +15,7 @@ - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [lchmod](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws for a symbolic link on Windows and Linux, whose hosts cannot change a link's own mode; on macOS the link's mode is changed. Anything else goes to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`, as FreeBSD 11.4's [`sys_lchmod`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2490-L2496) does, so a file or directory is changed as by `chmod`, with `fchmodat`'s path lookup and Windows mode mapping. On Windows any reparse point counts as a link. Console behaviour is unverified -- [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` +- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) query filesystem configuration values. `pathconf` looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255). `fpathconf` and `_fpathconf` operate on an open descriptor, returning `EBADF` if the descriptor is negative or not open. All three report the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. They throw for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does - [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c` From 4e7e4eed8faeda190e3fa96fa76d29998c67e468 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:19:04 +0200 Subject: [PATCH 219/318] fix(libkernel_sys): check the fpathconf descriptor with fstat DescriptorIsOpen_nid_no_patch is not extern "C", so the patched libkernel.prx does not export it and libkernel_sys.prx fails to load (patched_libraries: LoadLibraryEx error 127). Check the descriptor like fchown does: GuestFstat on Windows, fstat elsewhere, EBADF on failure. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel_sys/Export.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 880b594c50..63a7c829b4 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -372,7 +372,13 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { } std::int64_t APS5_VABI _fpathconf_nid_postfix(int fd, int name) { - if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return Fail(GuestEbadf); + if (fd < 0) return Fail(GuestEbadf); +#ifdef _WIN32 + if (GuestFstat(fd) != 0) return Fail(GuestEbadf); +#else + struct stat status{}; + if (::fstat(fd, &status) != 0) return Fail(GuestEbadf); +#endif return PathConfigurationValue(name, "fpathconf"); } From 01655d7219616ed7de71ff7fa488ca7ecf0442b6 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:43:22 +0200 Subject: [PATCH 220/318] fix(libkernel_sys): answer fpathconf for pipes and _PC_ASYNC_IO as FreeBSD does kern_fpathconf answers _PC_ASYNC_IO with _POSIX_ASYNCHRONOUS_IO (200112) after the descriptor lookup, and a pipe or socket (no vnode) with PIPE_BUF for _PC_PIPE_BUF and EINVAL for every other name. Drop the unused File.hpp include and the duplicate KernelErrors.hpp include. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libkernel_sys/Export.cpp | 12 ++++++++---- core/libs/tests/GuestKernelSys.cpp | 18 +++++++++++++++++- docs/dev/debt/libkernel_sys.md | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 63a7c829b4..867a3a52bc 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -29,8 +29,6 @@ #include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libc/include/CreationMask.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" -#include "prx/libkernel/File/include/File.hpp" -#include "prx/libkernel/KernelErrors.hpp" extern "C" int APS5_VABI getpid_nid_postfix(void); #ifdef _WIN32 #ifndef NOMINMAX @@ -52,11 +50,12 @@ namespace { #ifdef _WIN32 extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); static void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} -static int GuestFstat(int fd) { +static int GuestFstat(int fd, bool* withoutVnode = nullptr) { const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); struct _stat64 status{}; const int result = ::_fstat64(fd, &status); _set_thread_local_invalid_parameter_handler(previous); + if (result == 0 && withoutVnode != nullptr) *withoutVnode = (status.st_mode & _S_IFMT) == _S_IFIFO; return result; } #endif @@ -100,6 +99,7 @@ constexpr int PcRecXferAlign = 17; constexpr int PcSymlinkMax = 18; constexpr int PcMinHoleSize = 21; constexpr int PcAsyncIo = 53; +constexpr std::int64_t PosixAsynchronousIo = 200112; constexpr int PcPrioIo = 54; constexpr int PcSyncIo = 55; constexpr int PcAclExtended = 59; @@ -373,12 +373,16 @@ std::int64_t APS5_VABI pathconf_nid_postfix(const char* path, int name) { std::int64_t APS5_VABI _fpathconf_nid_postfix(int fd, int name) { if (fd < 0) return Fail(GuestEbadf); + bool withoutVnode = false; #ifdef _WIN32 - if (GuestFstat(fd) != 0) return Fail(GuestEbadf); + if (GuestFstat(fd, &withoutVnode) != 0) return Fail(GuestEbadf); #else struct stat status{}; if (::fstat(fd, &status) != 0) return Fail(GuestEbadf); + withoutVnode = S_ISFIFO(status.st_mode) || S_ISSOCK(status.st_mode); #endif + if (name == PcAsyncIo) return PosixAsynchronousIo; + if (withoutVnode) return name == PcPipeBuf ? 512 : Fail(GuestEinval); return PathConfigurationValue(name, "fpathconf"); } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 78bc8e9fdc..745c7b0346 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -340,12 +340,26 @@ void CheckDescriptorPathconf(F fn, int descriptor) { Require(fn(descriptor, 55) == 0); for (const int name : {0, 11, 61, 62, 65, -1}) Require(fn(descriptor, name) == -1 && Error() == 22); - for (const int name : {2, 3, 9, 10, 14, 16, 17, 21, 53, 59, 60, 63, 64}) + Require(fn(descriptor, 53) == 200112); + for (const int name : {2, 3, 9, 10, 14, 16, 17, 21, 59, 60, 63, 64}) Require(DescriptorPathconfThrows(fn, descriptor, name)); Require(fn(-1, 4) == -1 && Error() == 9); Require(fn(descriptor + 1000, 4) == -1 && Error() == 9); } +template +void CheckPipePathconf(F fn) { + int ends[2]{-1, -1}; + Require(pipe_nid_postfix(ends) == 0); + for (const int end : ends) { + Require(fn(end, 6) == 512); + Require(fn(end, 53) == 200112); + for (const int name : {0, 1, 4, 5, 7, 8, 12, 18, 54, 55, 61, -1}) + Require(fn(end, name) == -1 && Error() == 22); + } + Require(close_nid_postfix(ends[0]) == 0 && close_nid_postfix(ends[1]) == 0); +} + int main() { #ifdef _WIN32 auto hostDirectorySpelling = std::filesystem::current_path().native(); @@ -380,6 +394,8 @@ int main() { Require(fstat_nid_postfix(descriptor, nullptr) == -1 && Error() == 14); CheckDescriptorPathconf(fpathconf_nid_postfix, descriptor); CheckDescriptorPathconf(_fpathconf_nid_postfix, descriptor); + CheckPipePathconf(fpathconf_nid_postfix); + CheckPipePathconf(_fpathconf_nid_postfix); Require(close_nid_postfix(descriptor) == 0); Require(fstatfs_nid_postfix(-1, viaPublic) == -1 && Error() == 9); Require(fstat_nid_postfix(descriptor, &viaFstat) == -1 && Error() == 9); diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 085a67035e..5a784a62b7 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -15,7 +15,7 @@ - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [lchmod](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws for a symbolic link on Windows and Linux, whose hosts cannot change a link's own mode; on macOS the link's mode is changed. Anything else goes to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`, as FreeBSD 11.4's [`sys_lchmod`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2490-L2496) does, so a file or directory is changed as by `chmod`, with `fchmodat`'s path lookup and Windows mode mapping. On Windows any reparse point counts as a link. Console behaviour is unverified -- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) query filesystem configuration values. `pathconf` looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255). `fpathconf` and `_fpathconf` operate on an open descriptor, returning `EBADF` if the descriptor is negative or not open. All three report the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. They throw for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` +- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) query filesystem configuration values. `pathconf` looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255). `fpathconf` and `_fpathconf` operate on an open descriptor, returning `EBADF` if the descriptor is negative or not open, `_PC_ASYNC_IO` 200112 for any open descriptor and, for a pipe, `_PC_PIPE_BUF` 512 and `EINVAL` for every other name, as FreeBSD 11's `kern_fpathconf` does; a guest socket descriptor is not a host descriptor, so the `fstat` check reports it as `EBADF`, as `fchown` does. All three report the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. They throw for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, and for `pathconf` `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does - [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c` From 702d85e0bcf82546278cbd624cf5c1c5037ae5e7 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 00:57:29 +1300 Subject: [PATCH 221/318] fix(libSceNpCommon): check for a queued callout before writing it sceNpCalloutStartOnCtx64 cleared and filled the entry before looking for it in the queue, so restarting a queued callout cut off every callout queued after it and changed its deadline while the callout thread could read it. The entry is now written only after the duplicate check under the context mutex. sceNpArchInit and sceNpArchTerm now hold a lock, so a second caller no longer returns before the global heap exists. Both differences from the firmware are recorded in the debt document. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013XFS3FUfBtqCVfwmBrRQ6q --- core/libs/prx/libSceNpCommon/Runtime.cpp | 27 ++++++++++++++++-------- core/libs/tests/GuestNpCommonRuntime.cpp | 13 ++++++++++++ docs/dev/debt/libSceNpCommon.md | 2 ++ 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceNpCommon/Runtime.cpp b/core/libs/prx/libSceNpCommon/Runtime.cpp index ef8324be67..965506fa51 100644 --- a/core/libs/prx/libSceNpCommon/Runtime.cpp +++ b/core/libs/prx/libSceNpCommon/Runtime.cpp @@ -307,7 +307,8 @@ std::mt19937 g_random {std::random_device {}()}; NpMemoryHeap g_globalHeap {}; NpAllocator g_globalAllocator {}; NpAllocatorEx g_globalAllocatorEx {}; -std::atomic g_archReferences {0}; +int g_archReferences = 0; +std::mutex g_archMutex; std::mutex g_ipcClientMutex; bool g_ipcClientInitialized = false; NpMemoryHeap g_ipcClientHeap {}; @@ -527,14 +528,21 @@ int APS5_VABI sceNpIpc2ClientTerm(void) { } int APS5_VABI sceNpArchInit(void) { - if (g_archReferences.fetch_add(1) != 0) return 0; + std::lock_guard lock(g_archMutex); + if (g_archReferences != 0) { + ++g_archReferences; + return 0; + } const int result = InitMemoryHeap(&g_globalHeap, NP_GLOBAL_HEAP_SIZE, NP_GLOBAL_HEAP_NAME); - if (result < 0) g_archReferences.fetch_sub(1); - return result < 0 ? result : 0; + if (result < 0) return result; + g_archReferences = 1; + return 0; } void APS5_VABI sceNpArchTerm(void) { - if (g_archReferences.fetch_sub(1) == 1) DestroyMemoryHeap(&g_globalHeap); + std::lock_guard lock(g_archMutex); + if (g_archReferences == 0) return; + if (--g_archReferences == 0) DestroyMemoryHeap(&g_globalHeap); } int APS5_VABI sceNpHeapInit(NpHeap* heap, void* base, std::size_t size, const char* name) { @@ -724,10 +732,7 @@ void APS5_VABI sceNpCalloutTermCtx(NpCalloutContext* context) { int APS5_VABI sceNpCalloutStartOnCtx64(NpCalloutContext* context, NpCallout* callout, std::uint64_t usec, void (APS5_VABI *handler)(void* arg), void* arg) { if (context->initialized == 0) return NP_ERROR_NOT_INITIALIZED; - *callout = {}; - callout->deadline = ClockUsec() + usec; - callout->handler = handler; - callout->arg = arg; + const std::uint64_t deadline = ClockUsec() + usec; sceNpMutexLock(&context->mutex); for (NpCallout* entry = context->pending; entry != nullptr; entry = entry->next) { if (entry == callout) { @@ -735,6 +740,10 @@ int APS5_VABI sceNpCalloutStartOnCtx64(NpCalloutContext* context, NpCallout* cal return NP_ERROR_ALREADY_QUEUED; } } + *callout = {}; + callout->deadline = deadline; + callout->handler = handler; + callout->arg = arg; NpCallout** link = &context->pending; while (*link != nullptr && callout->deadline > (*link)->deadline) link = &(*link)->next; callout->next = *link; diff --git a/core/libs/tests/GuestNpCommonRuntime.cpp b/core/libs/tests/GuestNpCommonRuntime.cpp index 5056298baf..ff9f9aa995 100644 --- a/core/libs/tests/GuestNpCommonRuntime.cpp +++ b/core/libs/tests/GuestNpCommonRuntime.cpp @@ -294,6 +294,19 @@ void TestCallouts() { Require(sceNpCalloutStopOnCtx(&context, &stopped, &wasPending) == 0 && wasPending == 1, "a queued callout is removed"); Require(sceNpCalloutStopOnCtx(&context, &stopped, &wasPending) == 0 && wasPending == 0, "removing twice reports nothing pending"); + NpCallout queuedFirst {}; + NpCallout queuedSecond {}; + Require(sceNpCalloutStartOnCtx(&context, &queuedFirst, 30000000, RecordUnexpected, nullptr) == 0, "an earlier distant callout is queued"); + Require(sceNpCalloutStartOnCtx(&context, &queuedSecond, 60000000, RecordUnexpected, nullptr) == 0, "a later distant callout is queued"); + const std::uint64_t queuedDeadline = queuedFirst.deadline; + Require(sceNpCalloutStartOnCtx(&context, &queuedFirst, 0, RecordFirst, &context) == NpAlreadyQueued, + "the earlier callout cannot be queued again"); + Require(queuedFirst.next == &queuedSecond && queuedFirst.deadline == queuedDeadline && queuedFirst.handler == RecordUnexpected && + queuedFirst.arg == nullptr, "a rejected start leaves the queued callout unchanged"); + Require(sceNpCalloutStopOnCtx(&context, &queuedSecond, &wasPending) == 0 && wasPending == 1, + "the later callout is still pending after the rejected start"); + Require(sceNpCalloutStopOnCtx(&context, &queuedFirst, &wasPending) == 0 && wasPending == 1, "the earlier callout is still pending"); + NpCallout later {}; Require(sceNpCalloutStartOnCtx(&context, &later, 20000, RecordSecond, nullptr) == 0, "a later callout is queued"); Require(sceNpCalloutStartOnCtx(&context, &callout, 1000, RecordFirst, nullptr) == 0, "an earlier callout is queued"); diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index 32efbb235e..f18c9e79b3 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -47,6 +47,8 @@ The 35 declarations below use names mapped from the local `libSceNpCommon.sprx` - [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - writes the libkernel system software version as `major.minor` into an 8-byte buffer without checking it, as shadPS4 does; that version is the fixed `1.00`, not the SDK version a title was built with. - [The exports in Runtime.cpp](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [Strings.cpp](../../../core/libs/prx/libSceNpCommon/Strings.cpp) follow `libSceNpCommon.sprx` from firmware 12.00, read from disassembly and not measured on a console. - [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) always applies an explicit priority when one is given; the firmware picks between explicit and inherited scheduling from the title's compiled SDK version, which is not available here. +- [sceNpCalloutStartOnCtx and sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) look for the callout in the queue before writing it and return `0x80558006` with the entry and the queue unchanged. Firmware 12.00, read from disassembly and not measured on a console, clears and fills the entry before it takes the lock and checks the queue, so restarting a queued callout returns the same error but drops every callout queued after it. +- [sceNpArchInit and sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) keep a reference count under a lock and create the global heap once. Firmware 12.00, read from disassembly and not measured on a console, uses no lock and never sets its count on the first call, so every call made while the count is zero creates the heap again, and a call to sceNpArchTerm at zero destroys the heap and leaves the count negative. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) draws from a 32-bit Mersenne Twister seeded once per process; the console uses SFMT-19937 with a seed set inside the module, so the sequences differ. - [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) print the same information as the console in a different layout. - [The string-to-integer exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) clear `errno` before parsing; the firmware reads whatever `errno` held before the call when the result saturates. From fba61c0da01cf7fd4346cb94a6b3a9d86b68862c Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:01:12 +0200 Subject: [PATCH 222/318] docs(debt): shorten the pathconf entry under the length limit Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libkernel_sys.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 5a784a62b7..68c58a80fb 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -15,7 +15,7 @@ - [openat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [lchmod](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) throws for a symbolic link on Windows and Linux, whose hosts cannot change a link's own mode; on macOS the link's mode is changed. Anything else goes to `fchmodat(AT_FDCWD, path, mode, AT_SYMLINK_NOFOLLOW)`, as FreeBSD 11.4's [`sys_lchmod`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_syscalls.c#L2490-L2496) does, so a file or directory is changed as by `chmod`, with `fchmodat`'s path lookup and Windows mode mapping. On Windows any reparse point counts as a link. Console behaviour is unverified -- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) query filesystem configuration values. `pathconf` looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255). `fpathconf` and `_fpathconf` operate on an open descriptor, returning `EBADF` if the descriptor is negative or not open, `_PC_ASYNC_IO` 200112 for any open descriptor and, for a pipe, `_PC_PIPE_BUF` 512 and `EINVAL` for every other name, as FreeBSD 11's `kern_fpathconf` does; a guest socket descriptor is not a host descriptor, so the `fstat` check reports it as `EBADF`, as `fchown` does. All three report the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. They throw for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, and for `pathconf` `_PC_ASYNC_IO`); other names fail with `EINVAL` +- [pathconf, fpathconf, _fpathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) report FreeBSD 11's `ufs_pathconf` constants for files and directories alike (`_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0); whether the console's filesystems report the same is not verified. They throw for names whose UFS value depends on the mount, the kernel options or the page size, and where UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, and `_PC_ASYNC_IO` in `pathconf`); other names fail with `EINVAL`. `fpathconf` and `_fpathconf` answer `_PC_ASYNC_IO` 200112 and, for a pipe, `_PC_PIPE_BUF` 512 and `EINVAL` otherwise, as FreeBSD 11's `kern_fpathconf` does; a guest socket is not a host descriptor, so the `fstat` check reports it as `EBADF`, as in `fchown` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. On Windows, `mode` is ignored. Console behaviour is unverified - [readlink](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume (`\??\Volume{GUID}\`), any other name surrogate and every reparse point without the name surrogate bit (WOF compression, deduplication, cloud files) return `EINVAL`, as FreeBSD does for a file that is not a link and as CPython's `os.readlink`, which reads only symbolic links and junctions, does - [aio_read, aio_write, aio_fsync, aio_error, aio_return, aio_suspend, aio_waitcomplete, aio_cancel](../../../core/libs/prx/libkernel_sys/Aio.cpp) (libkernel_sys) - a request with `SIGEV_SIGNAL`, `SIGEV_THREAD_ID` or `SIGEV_KEVENT` completion, or on a socket, throws, because delivering a guest signal or kevent and socket aio are not modelled. One worker thread runs the process's requests one at a time in submission order, so a request is cancellable only until that thread takes it. A queued request keeps the descriptor number, not the open file: closing or reusing the descriptor before it runs changes what it reaches. Pipes fail with `EOPNOTSUPP`, as FreeBSD 11 does with `vfs.aio.enable_unsafe` 0; the console's setting is unverified. Waits are not interrupted by signals (`EINTR`), and timeouts under 1 ms round up to 1 ms (hz 1000). Otherwise the rules follow FreeBSD 11 `vfs_aio.c` From 84ac63232c4250105dbe14ca557f0c40dee74b28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0l=C3=A9gl?= Date: Tue, 20 Oct 2026 13:25:26 +0200 Subject: [PATCH 223/318] perf(agc): take captureValidation's records in one pass over the lookup log captureValidation gave each texture of a build its last lookup record by scanning the thread's lookup log backwards once per texture. A build keeps all of its own records in the log, so a build over a large image table read on average half of a log at least as long as the table for every texture: about 1.1 million record reads per capture for a 1,500-texture table. LastRecordIndices sorts the build's objects once and walks the log backwards once, stopping when every object has its record: the same records, in O((textures + records) log textures). Its scratch vectors are released like the log's when a large build has grown them past the log's bound. A graphics test compares LastRecordIndices with the backward scan over repeated records and objects, absent and null objects, a 1,500-texture table over its own records, over records looked up twice and over a log missing its first records, and random logs of up to 2,048 records. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 53 ++++++++++++--- .../prx/libSceAgcDriver/tests/Graphics.cpp | 1 + .../libSceAgcDriver/tests/GraphicsTests.hpp | 1 + .../tests/LastRecordIndices.cpp | 64 +++++++++++++++++++ 6 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/LastRecordIndices.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7e7d22b021..ffde30f90e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1903,6 +1903,7 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL tests/GuestSamplerResource.cpp tests/TextureDetiler.cpp tests/DepthSurfaceReuse.cpp + tests/LastRecordIndices.cpp ) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index c68ef833c5..d090ebd057 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -50,6 +50,7 @@ bool SampledTexturesShareEntry(std::span first, std::span words); bool ShaderStoresThroughAddresses(const ShaderRecompiler::SharedSpirv& spirv); +void LastRecordIndices(std::span logged, std::span objects, std::vector& indices); // The cached storage image of a surface (render targets use it as their resident image); brought up // to date with guest memory before it is returned. diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 863d6cd9cf..4d56d0db54 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -2911,21 +2912,57 @@ bool TemplateAuditDue(std::chrono::steady_clock::time_point last) { } +void LastRecordIndices(std::span logged, std::span objects, std::vector& indices) { + indices.assign(objects.size(), logged.size()); + thread_local std::vector>* wantedSlot = nullptr; + auto& wanted = ShaderRecompiler::ThreadOwned(wantedSlot); + wanted.clear(); + for (std::size_t i = 0; i < objects.size(); ++i) wanted.emplace_back(objects[i], i); + const auto byObject = [](const std::pair& left, const std::pair& right) { return std::less{}(left.first, right.first); }; + std::sort(wanted.begin(), wanted.end(), byObject); + std::size_t open = wanted.size(); + for (auto at = logged.size(); at-- > 0 && open != 0;) { + const auto [first, last] = std::equal_range(wanted.begin(), wanted.end(), std::pair{logged[at], 0}, byObject); + if (first == last || indices[first->second] != logged.size()) continue; + for (auto it = first; it != last; ++it) indices[it->second] = at; + open -= static_cast(last - first); + } + if (wanted.capacity() > LookupLogBound + LookupLogBound / 2) { + wanted.clear(); + wanted.shrink_to_fit(); + } +} + // Turns the lookups' records into this object's per-texture validation records (see // ValidatedSurface); the last record of an object is the freshest. void ShaderResources::captureValidation() { - const auto record = [](const void* object) -> const LookupRecord* { - const auto& log = lookupLog(); - for (auto it = log.rbegin(); it != log.rend(); ++it) { - if (it->object == object) return &*it; - } - return nullptr; - }; + const auto& log = lookupLog(); + thread_local std::vector* loggedSlot = nullptr; + auto& logged = ShaderRecompiler::ThreadOwned(loggedSlot); + thread_local std::vector* objectsSlot = nullptr; + auto& objects = ShaderRecompiler::ThreadOwned(objectsSlot); + thread_local std::vector* indicesSlot = nullptr; + auto& indices = ShaderRecompiler::ThreadOwned(indicesSlot); + logged.clear(); + for (const auto& entry : log) logged.push_back(entry.object); + objects.clear(); + for (const auto& texture : textures) objects.push_back(texture.get()); + LastRecordIndices(logged, objects, indices); validatedTextures.assign(textures.size(), {}); for (std::size_t i = 0; i < textures.size(); ++i) { - if (const auto* found = record(textures[i].get())) validatedTextures[i] = {found->resource, found->bytes, found->keys, found->generation, 0, found->source, found->depth, true}; + if (indices[i] == log.size()) continue; + const auto& found = log[indices[i]]; + validatedTextures[i] = {found.resource, found.bytes, found.keys, found.generation, 0, found.source, found.depth, true}; } lookupLog().clear(); + const auto trim = [](auto& scratch) { + if (scratch.capacity() <= LookupLogBound + LookupLogBound / 2) return; + scratch.clear(); + scratch.shrink_to_fit(); + }; + trim(logged); + trim(objects); + trim(indices); } std::size_t ShaderResources::LookupLogCapacity() { diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7b4e5bda3d..824bc8bc3b 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -11599,6 +11599,7 @@ int main(int argc, char** argv) { RunGuestTextureResourceTests(); RunDepthSurfaceReuseTests(); RunGuestSamplerResourceTests(); + RunLastRecordIndicesTests(); mock = MockVulkan{}; auto textureDetilerContext = mockContext(); textureDetilerContext.limits.minStorageBufferOffsetAlignment = 16; diff --git a/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp b/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp index 28a442b75d..95dcc30770 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/GraphicsTests.hpp @@ -36,6 +36,7 @@ void RunTextureTilingTests(); void RunGuestTextureResourceTests(); void RunDepthSurfaceReuseTests(); void RunGuestSamplerResourceTests(); +void RunLastRecordIndicesTests(); void RunTextureDetilerTests(const AgcDriver::Graphics::Context& context, const TextureDetilerTestAccess& access); #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/LastRecordIndices.cpp b/core/libs/prx/libSceAgcDriver/tests/LastRecordIndices.cpp new file mode 100644 index 0000000000..75b7a3ee21 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/LastRecordIndices.cpp @@ -0,0 +1,64 @@ +#include "GraphicsTests.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; + +std::vector BackwardScan(const std::vector& logged, const std::vector& objects) { + std::vector indices(objects.size(), logged.size()); + for (std::size_t i = 0; i < objects.size(); ++i) { + for (auto at = logged.size(); at-- > 0;) { + if (logged[at] == objects[i]) { + indices[i] = at; + break; + } + } + } + return indices; +} + +void CheckIndices(const std::vector& logged, const std::vector& objects, const std::string& what) { + std::vector indices; + LastRecordIndices(logged, objects, indices); + Require(indices == BackwardScan(logged, objects), "last record indices, " + what + ": an object did not get the index of its last record"); +} + +} + +void RunLastRecordIndicesTests() { + std::array pool{}; + const auto object = [&pool](std::size_t index) -> const void* { return &pool[index]; }; + + CheckIndices({object(0), object(1), object(0), object(2)}, {object(0), object(1), object(2), object(3), object(0), nullptr}, "repeated records and objects"); + CheckIndices({}, {object(0), nullptr}, "an empty log"); + CheckIndices({object(0), nullptr, object(1)}, {}, "no objects"); + CheckIndices({nullptr, object(0)}, {nullptr, object(0)}, "a null object"); + + std::vector table; + for (std::size_t texture = 0; texture < 1500; ++texture) table.push_back(object(texture)); + CheckIndices(table, table, "a table of 1500 textures over its own records in table order"); + std::vector repeated = table; + repeated.insert(repeated.end(), table.begin(), table.begin() + 548); + CheckIndices(repeated, table, "a table of 1500 textures whose first 548 were looked up twice"); + std::vector missing(table.begin() + 476, table.end()); + CheckIndices(missing, table, "a table of 1500 textures over a log without its first 476 records"); + + std::mt19937 generator(0x5eed); + for (const std::size_t records : {std::size_t{1}, std::size_t{7}, std::size_t{64}, std::size_t{1023}, std::size_t{1024}, std::size_t{1500}, std::size_t{2048}}) { + for (const std::size_t textures : {std::size_t{1}, std::size_t{5}, std::size_t{300}, std::size_t{1500}}) { + for (const std::size_t distinct : {std::size_t{8}, pool.size()}) { + std::vector logged(records); + for (auto& entry : logged) entry = object(generator() % distinct); + std::vector objects(textures); + for (auto& entry : objects) entry = object(generator() % distinct); + CheckIndices(logged, objects, std::to_string(records) + " records, " + std::to_string(textures) + " objects of " + std::to_string(distinct)); + } + } + } +} From bcaaf3924173fe27851109ca6af369878c4ac1b9 Mon Sep 17 00:00:00 2001 From: hypermist Date: Sat, 24 Oct 2026 01:06:42 +1300 Subject: [PATCH 224/318] docs(libSceNpCommon): cite firmware 13.60, not 12.00, for the decrypted modules The firmware version comes from the SDK 0x13600007 recorded in each module's module-param segment. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013XFS3FUfBtqCVfwmBrRQ6q --- docs/dev/debt/libSceNpCommon.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/debt/libSceNpCommon.md b/docs/dev/debt/libSceNpCommon.md index f18c9e79b3..da67369967 100644 --- a/docs/dev/debt/libSceNpCommon.md +++ b/docs/dev/debt/libSceNpCommon.md @@ -45,10 +45,10 @@ The 35 declarations below use names mapped from the local `libSceNpCommon.sprx` - [Online ID and NpId utilities](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - `sceNpCmpOnlineId`, `sceNpCmpNpIdInOrder`, `sceNpGetPlatformType` and `sceNpIntIsValidOnlineId` are not measured on a console; their signatures, checks and results follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp), including the platform tags read from `opt[4..7]` and the 3 to 16 character online ID rule. - [Mutex, condition and thread wrappers](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - the `sceNpMutex*`, `sceNpLwMutex*`, `sceNpCond*` and `sceNpJoinThread` wrappers forward to the libkernel `scePthread*` calls as in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/np/np_common.cpp) and are not measured on a console. Only `EBUSY` from try-lock and `ETIMEDOUT` from a timed wait are mapped to NP errors; every other kernel error is returned unchanged. The `flags` argument of `sceNpCondInit` is ignored. - [sceNpGetSdkVersion](../../../core/libs/prx/libSceNpCommon/Export.cpp) (libSceNpCommon) - writes the libkernel system software version as `major.minor` into an 8-byte buffer without checking it, as shadPS4 does; that version is the fixed `1.00`, not the SDK version a title was built with. -- [The exports in Runtime.cpp](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [Strings.cpp](../../../core/libs/prx/libSceNpCommon/Strings.cpp) follow `libSceNpCommon.sprx` from firmware 12.00, read from disassembly and not measured on a console. +- [The exports in Runtime.cpp](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [Strings.cpp](../../../core/libs/prx/libSceNpCommon/Strings.cpp) follow `libSceNpCommon.sprx` from firmware 13.60, read from disassembly and not measured on a console. - [sceNpCreateThread](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) always applies an explicit priority when one is given; the firmware picks between explicit and inherited scheduling from the title's compiled SDK version, which is not available here. -- [sceNpCalloutStartOnCtx and sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) look for the callout in the queue before writing it and return `0x80558006` with the entry and the queue unchanged. Firmware 12.00, read from disassembly and not measured on a console, clears and fills the entry before it takes the lock and checks the queue, so restarting a queued callout returns the same error but drops every callout queued after it. -- [sceNpArchInit and sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) keep a reference count under a lock and create the global heap once. Firmware 12.00, read from disassembly and not measured on a console, uses no lock and never sets its count on the first call, so every call made while the count is zero creates the heap again, and a call to sceNpArchTerm at zero destroys the heap and leaves the count negative. +- [sceNpCalloutStartOnCtx and sceNpCalloutStartOnCtx64](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) look for the callout in the queue before writing it and return `0x80558006` with the entry and the queue unchanged. Firmware 13.60, read from disassembly and not measured on a console, clears and fills the entry before it takes the lock and checks the queue, so restarting a queued callout returns the same error but drops every callout queued after it. +- [sceNpArchInit and sceNpArchTerm](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) keep a reference count under a lock and create the global heap once. Firmware 13.60, read from disassembly and not measured on a console, uses no lock and never sets its count on the first call, so every call made while the count is zero creates the heap again, and a call to sceNpArchTerm at zero destroys the heap and leaves the count negative. - [sceNpGetRandom](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) draws from a 32-bit Mersenne Twister seeded once per process; the console uses SFMT-19937 with a seed set inside the module, so the sequences differ. - [sceNpHeapShowStat](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) and [sceNpDbgDumpBinary](../../../core/libs/prx/libSceNpCommon/Runtime.cpp) print the same information as the console in a different layout. - [The string-to-integer exports](../../../core/libs/prx/libSceNpCommon/Strings.cpp) clear `errno` before parsing; the firmware reads whatever `errno` held before the call when the result saturates. From 5b2955b9cfcfc366e070aa55ca6a67ee399e32ef Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Fri, 23 Oct 2026 14:17:45 +0200 Subject: [PATCH 225/318] ci(build): let a running main build finish before the next push's build starts Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cab59565e5..3dbe96d99d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ permissions: concurrency: group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name != 'push' }} env: CONFIGURE: >- From 12ca5c16f0cc7c9aee25456e323e8c519c90cb9f Mon Sep 17 00:00:00 2001 From: Giuseppe Magliano Date: Fri, 23 Oct 2026 12:27:39 +0200 Subject: [PATCH 226/318] test(agc): skip the forced-watch shadow tests where imports report pages written agc_driver_bda_reads_shadowed_results, its per-build form and agc_unit_shadow_completion_store force host imports to stay watched on non-Windows hosts. On RADV, importing watched host memory and reading it from the GPU both report the pages written, which is why the driver compares those imports instead of watching them. Under the forced watch those reports read as CPU stores: every shadowed unit is stale-dropped before the address-based publish, and the tests fail on every run. Both tests now run the import probe and skip, like agc_unit_shadow and agc_shadowed_key_stores already do there, when the import or a GPU read reports pages written. Devices whose imports stay clean until a GPU write, such as lavapipe, still run them. --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index a5bc67e508..ec52550d47 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -5106,6 +5106,11 @@ bool addressShadowTests(const Device& device, Recorder& recorder) { } const auto decided = PrepareImportWatch(base); #ifndef _WIN32 + const auto probe = ProbeImportWriteProtection(base); + if (probe.failure != nullptr || probe.writtenAtImport != 0 || probe.writtenAfterSubmit != 0) { + std::cout << "skipped, the device reports watched host memory written when it imports or reads it\n"; + return false; + } SetImportWatch(base, ImportWatch::Watch); #endif struct Restore { @@ -5241,6 +5246,11 @@ bool completionStoreShadowTests(const Device& device, Recorder& recorder) { } const auto decided = PrepareImportWatch(base); #ifndef _WIN32 + const auto probe = ProbeImportWriteProtection(base); + if (probe.failure != nullptr || probe.writtenAtImport != 0 || probe.writtenAfterSubmit != 0) { + std::cout << "skipped, the device reports watched host memory written when it imports or reads it\n"; + return false; + } SetImportWatch(base, ImportWatch::Watch); #endif struct Restore { From eb9491b1949870eee518df39343d4d376a5d1f89 Mon Sep 17 00:00:00 2001 From: HQasper <52220032+HQasper@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:31:01 +0000 Subject: [PATCH 227/318] feat(libSceLibreSsl): implement RAND and the CRYPTO allocator functions RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free and CRYPTO_set_mem_functions threw. They now behave as in LibreSSL 2.6.5 through 3.4.3, where these functions did not change: RAND_bytes fills the buffer from host entropy and returns 1, RAND_add ignores its input, RAND_status returns 1, RAND_load_file returns 123456 for -1 and the requested count otherwise, RAND_file_name copies "/dev/urandom" with strlcpy semantics, CRYPTO_malloc and CRYPTO_free use the guest heap, and CRYPTO_set_mem_functions refuses with 0. CRYPTO_malloc of zero or a negative size throws because later LibreSSL versions take a size_t and call malloc instead of returning null. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 13 ++- core/libs/prx/libSceLibreSsl/Export.cpp | 40 --------- core/libs/prx/libSceLibreSsl/src/Runtime.cpp | 56 ++++++++++++ core/libs/tests/GuestLibreSslRuntime.cpp | 94 ++++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 3 +- 5 files changed, 164 insertions(+), 42 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/src/Runtime.cpp create mode 100644 core/libs/tests/GuestLibreSslRuntime.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 78e8a9adec..ad5ccb6a95 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -1,4 +1,4 @@ -add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp) +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Digest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Runtime.cpp) target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) configure_windows_unwind(libSceLibreSsl) @@ -36,4 +36,15 @@ if(BUILD_TESTING) set_tests_properties(guest_libressl_des PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_libressl_runtime_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslRuntime.cpp) + target_include_directories(guest_libressl_runtime_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_runtime_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_runtime_tests) + add_test(NAME guest_libressl_runtime COMMAND guest_libressl_runtime_tests) + set_tests_properties(guest_libressl_runtime PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_runtime PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 1e2c3c9caa..c18fb7ef2c 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -283,16 +283,6 @@ int APS5_VABI CRYPTO_cleanup_all_ex_data_nid_postfix(void) { return 0; } -int APS5_VABI CRYPTO_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CRYPTO_malloc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI CRYPTO_num_locks_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -308,11 +298,6 @@ int APS5_VABI CRYPTO_set_locking_callback_nid_postfix(void) { return 0; } -int APS5_VABI CRYPTO_set_mem_functions_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - void APS5_VABI DES_ecb_encrypt_nid_postfix(const unsigned char* input, unsigned char* output, const unsigned char* schedule, int enc) { if (input == nullptr || output == nullptr || schedule == nullptr) APS5_INVALID_ARG_EX; mbedtls_des_context context; @@ -1017,31 +1002,6 @@ int APS5_VABI PKCS12_parse_nid_postfix(void) { return 0; } -int APS5_VABI RAND_add_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI RAND_bytes_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI RAND_file_name_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI RAND_load_file_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI RAND_status_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI RSA_flags_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/Runtime.cpp b/core/libs/prx/libSceLibreSsl/src/Runtime.cpp new file mode 100644 index 0000000000..d7ce6ed69b --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Runtime.cpp @@ -0,0 +1,56 @@ +#include +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/HostEntropy.hpp" + +extern "C" { +void* APS5_VABI malloc_nid_postfix(std::size_t size); +void APS5_VABI free_nid_postfix(void* pointer); + +void APS5_VABI RAND_add_nid_postfix(const void*, int, double) { +} + +int APS5_VABI RAND_status_nid_postfix(void) { + return 1; +} + +int APS5_VABI RAND_bytes_nid_postfix(unsigned char* buffer, int count) { + if (count > 0) { + if (!buffer) throw std::invalid_argument(std::string(__func__) + ": null buffer"); + HostEntropy_nid_no_patch(buffer, static_cast(count)); + } + return 1; +} + +int APS5_VABI RAND_load_file_nid_postfix(const char*, std::int64_t bytes) { + return bytes == -1 ? 123456 : static_cast(bytes); +} + +const char* APS5_VABI RAND_file_name_nid_postfix(char* buffer, std::size_t size) { + static constexpr char path[] = "/dev/urandom"; + if (size) { + if (!buffer) throw std::invalid_argument(std::string(__func__) + ": null buffer"); + const std::size_t copied = size - 1 < sizeof(path) - 1 ? size - 1 : sizeof(path) - 1; + std::memcpy(buffer, path, copied); + buffer[copied] = '\0'; + } + return sizeof(path) - 1 >= size ? nullptr : buffer; +} + +void* APS5_VABI CRYPTO_malloc_nid_postfix(int count, const char*, int) { + if (count <= 0) NotImplemented_nid_no_patch("CRYPTO_malloc: zero or negative size"); + return malloc_nid_postfix(static_cast(count)); +} + +void APS5_VABI CRYPTO_free_nid_postfix(void* pointer) { + free_nid_postfix(pointer); +} + +int APS5_VABI CRYPTO_set_mem_functions_nid_postfix(void*, void*, void*) { + return 0; +} + +} diff --git a/core/libs/tests/GuestLibreSslRuntime.cpp b/core/libs/tests/GuestLibreSslRuntime.cpp new file mode 100644 index 0000000000..5a46198e17 --- /dev/null +++ b/core/libs/tests/GuestLibreSslRuntime.cpp @@ -0,0 +1,94 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +void APS5_VABI RAND_add_nid_postfix(const void*, int, double); +int APS5_VABI RAND_status_nid_postfix(void); +int APS5_VABI RAND_bytes_nid_postfix(unsigned char*, int); +int APS5_VABI RAND_load_file_nid_postfix(const char*, std::int64_t); +const char* APS5_VABI RAND_file_name_nid_postfix(char*, std::size_t); +void* APS5_VABI CRYPTO_malloc_nid_postfix(int, const char*, int); +void APS5_VABI CRYPTO_free_nid_postfix(void*); +int APS5_VABI CRYPTO_set_mem_functions_nid_postfix(void*, void*, void*); +void APS5_VABI free_nid_postfix(void*); +void* APS5_VABI malloc_nid_postfix(std::size_t); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (condition) return; + std::fprintf(stderr, "LibreSSL runtime check failed at line %u\n", location.line()); + std::abort(); +} + +template static void RequireThrows(TAction action, std::source_location location = std::source_location::current()) { + try { action(); } catch (const std::exception&) { return; } + Require(false, location); +} + +static void InitializeHeap() { + void* const api[10] {}; + ApplicationHeapRegister_nid_no_patch(api); +} + +int main() { + InitializeHeap(); + std::array first {}; + std::array second {}; + first.fill(0xa5); + Require(RAND_bytes_nid_postfix(first.data(), 0) == 1 && RAND_bytes_nid_postfix(first.data(), -5) == 1); + Require(RAND_bytes_nid_postfix(nullptr, 0) == 1); + Require(std::all_of(first.begin(), first.end(), [](unsigned char value) { return value == 0xa5; })); + Require(RAND_bytes_nid_postfix(first.data(), 3) == 1); + Require(std::all_of(first.begin() + 3, first.end(), [](unsigned char value) { return value == 0xa5; })); + Require(RAND_bytes_nid_postfix(first.data(), static_cast(first.size())) == 1); + Require(RAND_bytes_nid_postfix(second.data(), static_cast(second.size())) == 1); + Require(first != second); + std::array counts {}; + for (const auto value : first) ++counts[value]; + Require(*std::max_element(counts.begin(), counts.end()) < 64); + RequireThrows([] { RAND_bytes_nid_postfix(nullptr, 1); }); + + RAND_add_nid_postfix(first.data(), 16, 8.0); + RAND_add_nid_postfix(nullptr, 0, 0.0); + Require(RAND_status_nid_postfix() == 1); + Require(RAND_load_file_nid_postfix("/nonexistent", -1) == 123456); + Require(RAND_load_file_nid_postfix(nullptr, 1024) == 1024 && RAND_load_file_nid_postfix(nullptr, 0) == 0); + Require(RAND_load_file_nid_postfix(nullptr, 0x100000005) == 5); + + char name[32]; + std::memset(name, 'x', sizeof(name)); + Require(RAND_file_name_nid_postfix(name, sizeof(name)) == name && !std::strcmp(name, "/dev/urandom")); + std::memset(name, 'x', sizeof(name)); + Require(RAND_file_name_nid_postfix(name, 13) == name && !std::strcmp(name, "/dev/urandom")); + std::memset(name, 'x', sizeof(name)); + Require(RAND_file_name_nid_postfix(name, 12) == nullptr && !std::strcmp(name, "/dev/urando")); + std::memset(name, 'x', sizeof(name)); + Require(RAND_file_name_nid_postfix(name, 0) == nullptr && name[0] == 'x'); + Require(RAND_file_name_nid_postfix(nullptr, 0) == nullptr); + RequireThrows([] { RAND_file_name_nid_postfix(nullptr, 1); }); + + auto* block = static_cast(CRYPTO_malloc_nid_postfix(64, "file.c", 10)); + Require(block != nullptr); + std::memset(block, 0x5a, 64); + CRYPTO_free_nid_postfix(block); + block = static_cast(CRYPTO_malloc_nid_postfix(32, nullptr, 0)); + Require(block != nullptr); + free_nid_postfix(block); + block = static_cast(malloc_nid_postfix(16)); + CRYPTO_free_nid_postfix(block); + CRYPTO_free_nid_postfix(nullptr); + RequireThrows([] { CRYPTO_malloc_nid_postfix(0, nullptr, 0); }); + RequireThrows([] { CRYPTO_malloc_nid_postfix(-1, nullptr, 0); }); + Require(CRYPTO_set_mem_functions_nid_postfix(reinterpret_cast(&std::malloc), reinterpret_cast(&std::realloc), reinterpret_cast(&std::free)) == 0); + Require(CRYPTO_set_mem_functions_nid_postfix(nullptr, nullptr, nullptr) == 0); + std::puts("libSceLibreSsl runtime tests passed"); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index f84ef0fbf6..43069ab74b 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,8 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 373 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 365 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. +- [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` throws for a size of zero or less, which returns null up to 3.4 and calls `malloc` with a `size_t` size in later versions. A null buffer with a positive length throws From 2df7e993d3aca84c7378aeb2f3fe9082bc82a6bb Mon Sep 17 00:00:00 2001 From: mitul72 Date: Tue, 20 Oct 2026 16:29:18 -0600 Subject: [PATCH 228/318] fix(agc): answer a collect memo hit with the generation of the last finished tracker update A per-thread collect memo hit read the tracker generation without the tracker mutex. A walk running on another thread bumps the generation before it stamps the blocks it finds written, so the hit could return a value that walk then stamps blocks with; a caller that recorded it as its verified generation trusts those blocks in every later UnchangedSince and never refreshes them. The tracker now publishes the generation each time an update releases its mutex (TrackerLock), after every stamp of that update is in place, and a memo hit returns the published value. MarkWritten's release around beforeUnwatch does not publish, since it stamps again after relocking. The per-epoch collect map hit returns the published value too. Completed MarkWritten bumps are still seen at once; a value older than the live one only makes a check fail and refresh. agc_collect_memo_generation links GuestMemory against a fake write watch that holds a walk after its generation bump, takes a memo hit on another thread meanwhile, and checks the write the walk then stamps is not trusted against the generation the hit returned. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 13 ++ .../Execution/src/GuestMemory.cpp | 62 +++++++-- .../tests/CollectMemoGeneration.cpp | 127 ++++++++++++++++++ 3 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/CollectMemoGeneration.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7e7d22b021..a1c8a069b1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -748,6 +748,19 @@ if(BUILD_TESTING) endif() endif() +if(BUILD_TESTING AND NOT WIN32) + add_test_executable(agc_collect_memo_generation_tests + tests/CollectMemoGeneration.cpp + Execution/src/GuestMemory.cpp + Execution/src/PerformanceTraceThread.cpp + ) + target_include_directories(agc_collect_memo_generation_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(agc_collect_memo_generation_tests PRIVATE shader_recompiler_agc_driver libc) + set_target_properties(agc_collect_memo_generation_tests PROPERTIES CXX_EXTENSIONS OFF) + add_test(NAME agc_collect_memo_generation COMMAND agc_collect_memo_generation_tests) + set_tests_properties(agc_collect_memo_generation PROPERTIES TIMEOUT 30) +endif() + add_test_executable(agc_thread_priority_tests EXCLUDE_FROM_ALL tests/ThreadPriority.cpp Execution/src/ThreadPriority.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 69341f8385..c89d0cf96b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -878,6 +878,7 @@ struct WriteTracker { // bumps, or an image refreshed after a stamp would keep failing UnchangedSince until the next // walk of its range and re-upload at every use of the epoch. std::atomic generation{1}; + std::atomic stable{1}; std::vector pages; // Collect memo: page-rounded ranges walked during the current epoch (see BumpCollectEpoch). A // collect fully inside one skips GetWriteWatch, whose cost is proportional to the range's pages, @@ -1229,16 +1230,51 @@ WriteTracker& Tracker() { // Takes the tracker mutex; under APS5_PROFILE_DRAW an acquisition that found it held is counted // ('tracker waits N / M' in [guestmem]), which says whether walking outside the lock would pay. -std::unique_lock lockTracker(WriteTracker& tracker) { - static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; - if (!profile) return std::unique_lock(tracker.mutex); - std::unique_lock lock(tracker.mutex, std::try_to_lock); - if (!lock.owns_lock()) { - trackerWaits.fetch_add(1, std::memory_order_relaxed); +class TrackerLock { +public: + explicit TrackerLock(WriteTracker& tracker) : tracker(tracker), lock(tracker.mutex, std::defer_lock) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (!profile) { + lock.lock(); + return; + } + if (!lock.try_lock()) { + trackerWaits.fetch_add(1, std::memory_order_relaxed); + lock.lock(); + } + trackerAcquisitions.fetch_add(1, std::memory_order_relaxed); + } + ~TrackerLock() { + if (lock.owns_lock()) publish(); + } + TrackerLock(const TrackerLock&) = delete; + TrackerLock& operator=(const TrackerLock&) = delete; + + void Unlock() { + publish(); + lock.unlock(); + } + + void Suspend() { + lock.unlock(); + } + + void Resume() { lock.lock(); } - trackerAcquisitions.fetch_add(1, std::memory_order_relaxed); - return lock; + +private: + void publish() { + const auto current = tracker.generation.load(std::memory_order_relaxed); + if (tracker.stable.load(std::memory_order_relaxed) != current) tracker.stable.store(current, std::memory_order_release); + } + + WriteTracker& tracker; + std::unique_lock lock; +}; + +TrackerLock lockTracker(WriteTracker& tracker) { + return TrackerLock(tracker); } #ifdef _WIN32 @@ -1411,7 +1447,7 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi const auto& map = epochCollectMap(epoch); if (const auto found = map.ranges.find(first); found != map.ranges.end() && found->second.unwatched == unwatched && found->second.mapping == mapping && stop <= found->second.end) { collectMemoHits.fetch_add(1, std::memory_order_relaxed); - return tracker.generation.load(std::memory_order_relaxed); + return tracker.stable.load(std::memory_order_acquire); } } for (const auto& entry : threadCollectMemo().entries) { @@ -1420,7 +1456,7 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi // overlapping collect) were written before this caller reads, and an older value would // make every later UnchangedSince fail until the epoch ends. collectMemoHits.fetch_add(1, std::memory_order_relaxed); - return tracker.generation.load(std::memory_order_relaxed); + return tracker.stable.load(std::memory_order_acquire); } } } @@ -1609,7 +1645,7 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function auto lock = lockTracker(tracker); tracker.initialize(); if (!tracker.watched) { - lock.unlock(); + lock.Unlock(); return import(); } constexpr std::uint64_t page = 4096; @@ -1789,9 +1825,9 @@ static std::uint64_t markWritten(std::uint64_t address, std::size_t bytes, bool const bool collected = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast(blockFirst), static_cast(blockStop - blockFirst), &stampWrittenRun, &runs); #endif if (!collected) { - lock.unlock(); + lock.Suspend(); beforeUnwatch(blockFirst, static_cast(blockStop - blockFirst)); - lock.lock(); + lock.Resume(); unwatchLocked(tracker, blockFirst, static_cast(blockStop - blockFirst)); tracker.noteUnstampedStore(address, address + bytes); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/CollectMemoGeneration.cpp b/core/libs/prx/libSceAgcDriver/tests/CollectMemoGeneration.cpp new file mode 100644 index 0000000000..90d5e8a215 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/CollectMemoGeneration.cpp @@ -0,0 +1,127 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libc/include/GuestWriteWatch.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint64_t Block = 65536; +constexpr std::uint64_t Page = 4096; +constexpr std::uint64_t Base = std::uint64_t{1} << 32; +constexpr std::chrono::seconds Patience{10}; + +struct FakeWatch { + std::mutex mutex; + std::condition_variable changed; + std::vector> written; + bool holdNextWalk = false; + bool holding = false; + bool released = false; + bool timedOut = false; +}; + +FakeWatch& Fake() { + static FakeWatch fake; + return fake; +} + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +} + +namespace GuestWriteWatch { + +extern "C" { + +bool GuestWriteWatchAvailable_nid_postfix() { + return true; +} + +bool GuestWriteWatchCovers_nid_postfix(std::uintptr_t address, std::size_t bytes) { + return address >= Base && bytes <= 2 * Block && address - Base <= 2 * Block - bytes; +} + +bool GuestWriteWatchCollect_nid_postfix(std::uintptr_t address, std::size_t bytes, void (*written)(void* context, std::uintptr_t begin, std::uintptr_t end), void* context) { + auto& fake = Fake(); + const auto end = address + bytes; + std::vector> runs; + { + std::unique_lock lock(fake.mutex); + if (fake.holdNextWalk) { + fake.holdNextWalk = false; + fake.holding = true; + fake.changed.notify_all(); + fake.timedOut = !fake.changed.wait_for(lock, Patience, [&] { return fake.released; }); + } + for (auto it = fake.written.begin(); it != fake.written.end();) { + if (it->first < end && address < it->second) { + runs.emplace_back(std::max(it->first, address), std::min(it->second, end)); + it = fake.written.erase(it); + } else ++it; + } + } + for (const auto& [from, to] : runs) written(context, from, to); + return true; +} + +} + +} + +namespace { + +using namespace AgcDriver::GuestMemory; + +void CheckMemoHitDuringWalk() { + auto& fake = Fake(); + BumpCollectEpoch(); + const auto collected = CollectWrites(Base, Block); + Require(collected != 0 && UnchangedSince(Base, Block, collected), "the block was not collected"); + { + const std::lock_guard lock(fake.mutex); + fake.written.emplace_back(Base + Page, Base + 2 * Page); + fake.holdNextWalk = true; + } + std::thread walker([] { CollectWritesUncached(Base, 2 * Block); }); + bool holding = false; + { + std::unique_lock lock(fake.mutex); + holding = fake.changed.wait_for(lock, Patience, [&] { return fake.holding; }); + } + const auto memoized = holding ? CollectWrites(Base, Block) : 0; + { + const std::lock_guard lock(fake.mutex); + fake.released = true; + } + fake.changed.notify_all(); + walker.join(); + Require(holding, "the second walk did not reach the write watch"); + Require(!fake.timedOut, "the memoized collect waited for the walk in progress"); + Require(!UnchangedSince(Base + Page, Page, memoized), "a memoized collect returned the generation a walk in progress then stamped a write with"); + BumpCollectEpoch(); + Require(CollectWrites(Base, Block) > memoized && !UnchangedSince(Base, Block, memoized), "a later epoch trusts the write stamped by the walk in progress"); +} + +} + +int main() { + try { + CheckMemoHitDuringWalk(); + } catch (const std::exception& error) { + std::cerr << "collect memo generation test failed: " << error.what() << "\n"; + return 1; + } + std::cout << "collect memo generation test passed\n"; + return 0; +} From 687e7d637eca5b2576c8f14d921362d0f1f216ef Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 13:15:04 +0000 Subject: [PATCH 229/318] fix(libSceLibreSsl): return null from CRYPTO_malloc for sizes of zero or less CRYPTO_malloc threw for a size of zero or less. LibreSSL 2.5.4, 3.4.1 and 3.4.3 all take an int size and return null there, so it now does the same, and the test checks for null instead of a throw. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libSceLibreSsl/src/Runtime.cpp | 2 +- core/libs/tests/GuestLibreSslRuntime.cpp | 4 ++-- docs/dev/debt/libSceLibreSsl.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/src/Runtime.cpp b/core/libs/prx/libSceLibreSsl/src/Runtime.cpp index d7ce6ed69b..14f1cb5572 100644 --- a/core/libs/prx/libSceLibreSsl/src/Runtime.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Runtime.cpp @@ -41,7 +41,7 @@ const char* APS5_VABI RAND_file_name_nid_postfix(char* buffer, std::size_t size) } void* APS5_VABI CRYPTO_malloc_nid_postfix(int count, const char*, int) { - if (count <= 0) NotImplemented_nid_no_patch("CRYPTO_malloc: zero or negative size"); + if (count <= 0) return nullptr; return malloc_nid_postfix(static_cast(count)); } diff --git a/core/libs/tests/GuestLibreSslRuntime.cpp b/core/libs/tests/GuestLibreSslRuntime.cpp index 5a46198e17..937036d5fd 100644 --- a/core/libs/tests/GuestLibreSslRuntime.cpp +++ b/core/libs/tests/GuestLibreSslRuntime.cpp @@ -86,8 +86,8 @@ int main() { block = static_cast(malloc_nid_postfix(16)); CRYPTO_free_nid_postfix(block); CRYPTO_free_nid_postfix(nullptr); - RequireThrows([] { CRYPTO_malloc_nid_postfix(0, nullptr, 0); }); - RequireThrows([] { CRYPTO_malloc_nid_postfix(-1, nullptr, 0); }); + Require(CRYPTO_malloc_nid_postfix(0, nullptr, 0) == nullptr); + Require(CRYPTO_malloc_nid_postfix(-1, nullptr, 0) == nullptr); Require(CRYPTO_set_mem_functions_nid_postfix(reinterpret_cast(&std::malloc), reinterpret_cast(&std::realloc), reinterpret_cast(&std::free)) == 0); Require(CRYPTO_set_mem_functions_nid_postfix(nullptr, nullptr, nullptr) == 0); std::puts("libSceLibreSsl runtime tests passed"); diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 43069ab74b..29a95f22e3 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -6,4 +6,4 @@ - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); the module's LibreSSL version is unknown and nothing is checked on the console. Behaviour that differs between LibreSSL versions throws: `SHA256` with a null digest buffer (older versions return a static buffer) and `SHA512_Final` with the SHA-512/224 or SHA-512/256 digest length. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. -- [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` throws for a size of zero or less, which returns null up to 3.4 and calls `malloc` with a `size_t` size in later versions. A null buffer with a positive length throws +- [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` returns null for a size of zero or less, as LibreSSL 2.5.4 to 3.4 do; later versions take a `size_t` size and call `malloc`. A null buffer with a positive length throws From 2fd0610bffa21401a7ebd534a0b0d667bf82749f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 23 Oct 2026 15:21:46 +0200 Subject: [PATCH 230/318] fix(agc): draw a last provoking vertex that nothing observes without the extension Co-Authored-By: Claude --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 + .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 64 +++++++++++ .../prx/libSceAgcDriver/tests/Graphics.cpp | 107 +++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 174 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 7e7d22b021..054830b47f 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -3170,12 +3170,15 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_sample_location_grid_fallback COMMAND agc_driver_graphics_tests --sample-location-grid-fallback) set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_unobserved_provoking_vertex COMMAND agc_driver_graphics_tests --unobserved-provoking-vertex) + set_tests_properties(agc_driver_unobserved_provoking_vertex PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_sample_order_planning COMMAND agc_driver_graphics_tests --sample-order) set_tests_properties(agc_driver_sample_order_planning PROPERTIES TIMEOUT 30) if(WIN32) set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + set_tests_properties(agc_driver_unobserved_provoking_vertex PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") set_tests_properties(agc_driver_sample_order_planning PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index d7a28cebfb..4edd91fd5c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -124,6 +124,70 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp } return; } + struct Decorations { + std::unordered_map builtIns; + std::vector perVertexValues; + std::vector perPrimitiveValues; + }; + const auto readDecorations = [](const ShaderRecompiler::RecompileResult& program, Decorations& decorations) { + const auto words = program.spirv.Words(); + if (words.size() < 5) return false; + constexpr std::uint32_t NoMember = 0xffffffffu; + const auto key = [](std::uint32_t target, std::uint32_t member) { return (static_cast(target) << 32u) | member; }; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto count = words[cursor] >> 16u; + if (count == 0 || count > words.size() - cursor) return false; + const auto op = static_cast(words[cursor] & 0xffffu); + const std::size_t decoration = op == spv::OpDecorate ? 2 : op == spv::OpMemberDecorate ? 3 : 0; + if (decoration != 0 && count > decoration) { + const auto target = key(words[cursor + 1], op == spv::OpMemberDecorate ? words[cursor + 2] : NoMember); + if (words[cursor + decoration] == spv::DecorationFlat || words[cursor + decoration] == spv::DecorationPerVertexKHR) decorations.perVertexValues.push_back(target); + if (words[cursor + decoration] == spv::DecorationPerPrimitiveEXT) decorations.perPrimitiveValues.push_back(target); + if (words[cursor + decoration] == spv::DecorationBuiltIn && count > decoration + 1) decorations.builtIns[target] = words[cursor + decoration + 1]; + } + cursor += count; + } + return true; + }; + const auto readsProvokingVertex = [&](const ShaderRecompiler::RecompileResult& program) { + if (std::any_of(program.fragmentParameters.begin(), program.fragmentParameters.end(), [](const ShaderRecompiler::FragmentParameter& parameter) { + return parameter.flat || parameter.perVertex || parameter.custom || parameter.flatP10; + })) return true; + Decorations decorations; + if (!readDecorations(program, decorations)) return true; + const auto& builtIns = decorations.builtIns; + return std::any_of(decorations.perVertexValues.begin(), decorations.perVertexValues.end(), [&](std::uint64_t target) { + const auto builtIn = builtIns.find(target); + if (builtIn == builtIns.end()) return true; + switch (builtIn->second) { + case spv::BuiltInFragCoord: + case spv::BuiltInPointCoord: + case spv::BuiltInFrontFacing: + case spv::BuiltInSampleId: + case spv::BuiltInSamplePosition: + case spv::BuiltInSampleMask: + case spv::BuiltInHelperInvocation: + case spv::BuiltInSubgroupSize: + case spv::BuiltInSubgroupLocalInvocationId: + return false; + default: + return true; + } + }); + }; + const auto writesProvokingVertexBuiltIn = [&](const ShaderRecompiler::RecompileResult& program) { + Decorations decorations; + if (!readDecorations(program, decorations)) return true; + return std::any_of(decorations.builtIns.begin(), decorations.builtIns.end(), [&](const auto& builtIn) { + return (builtIn.second == spv::BuiltInLayer || builtIn.second == spv::BuiltInViewportIndex) && + std::find(decorations.perPrimitiveValues.begin(), decorations.perPrimitiveValues.end(), builtIn.first) == decorations.perPrimitiveValues.end(); + }); + }; + const bool observed = std::any_of(shaders.begin(), shaders.end(), [&](const CompiledShader& shader) { + if (shader.stage == ShaderRecompiler::ShaderStage::Fragment) return shader.program == nullptr || readsProvokingVertex(*shader.program); + return shader.program != nullptr && writesProvokingVertexBuiltIn(*shader.program); + }); + if (!context.provokingVertexLast && !observed) return; Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); const bool meshPrimitives = state.stages.path == ShaderPath::Geometry && state.stages.mesh; Require(!state.stages.tessellation && (meshPrimitives || (state.stages.path == ShaderPath::Vertex && !state.stages.mesh)), "last provoking vertex is unsupported for generated primitive pipelines"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 7b4e5bda3d..144d5a5d79 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -9354,7 +9354,7 @@ void ProvokingVertexTests() { ValidateProvokingVertex(unsupported, first); auto last = first; last.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; - expectFailure([&] { ValidateProvokingVertex(unsupported, last); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); + ValidateProvokingVertex(unsupported, last); auto supported = unsupported; supported.provokingVertexLast = true; ValidateProvokingVertex(supported, last); @@ -9477,6 +9477,105 @@ void ProvokingVertexTests() { expectFailure([&] { ValidateProvokingVertex(supported, last); }, "invalid provoking vertex mode"); } +void UnobservedProvokingVertexTests() { + using namespace AgcDriver::Graphics; + auto last = DecodeState(makeState()); + last.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT; + const Context unsupported{}; + const auto module = [](std::initializer_list instructions) { + std::vector words{spv::MagicNumber, 0x10300u, 0u, 4u, 0u}; + words.insert(words.end(), instructions); + return words; + }; + ShaderRecompiler::RecompileResult fragment{}; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + const ShaderRecompiler::FragmentParameter smooth{0, 0, false, false}; + const auto refused = [&](const State& state) { + expectFailure([&] { ValidateProvokingVertex(unsupported, state, shaders); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); + }; + for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_POINT_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_LIST, VK_PRIMITIVE_TOPOLOGY_LINE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN}) { + auto state = last; + state.topology = topology; + ValidateProvokingVertex(unsupported, state); + fragment.fragmentParameters.clear(); + fragment.spirv = module({}); + ValidateProvokingVertex(unsupported, state, shaders); + fragment.fragmentParameters = {smooth}; + fragment.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationLocation, 0u}); + ValidateProvokingVertex(unsupported, state, shaders); + for (std::uint32_t read = 0; read < 4; ++read) { + const ShaderRecompiler::FragmentParameter observed{1, 1, read == 0, read == 1, read == 2, read == 3, false}; + fragment.fragmentParameters = {smooth, observed}; + refused(state); + } + fragment.fragmentParameters = {smooth}; + for (const std::uint32_t decoration : {static_cast(spv::DecorationFlat), static_cast(spv::DecorationPerVertexKHR)}) { + fragment.spirv = module({(3u << 16u) | spv::OpDecorate, 1u, decoration}); + refused(state); + fragment.spirv = module({(4u << 16u) | spv::OpMemberDecorate, 1u, 0u, decoration}); + refused(state); + } + fragment.spirv = module({(5u << 16u) | spv::OpDecorate, 1u, spv::DecorationLocation}); + refused(state); + fragment.spirv = std::vector{}; + refused(state); + } + fragment.fragmentParameters = {smooth}; + for (const std::uint32_t generated : {static_cast(spv::BuiltInSampleId), static_cast(spv::BuiltInFrontFacing), static_cast(spv::BuiltInSubgroupLocalInvocationId)}) { + fragment.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, generated, (3u << 16u) | spv::OpDecorate, 1u, spv::DecorationFlat}); + ValidateProvokingVertex(unsupported, last, shaders); + fragment.spirv = module({(3u << 16u) | spv::OpDecorate, 1u, spv::DecorationFlat, (4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, generated}); + ValidateProvokingVertex(unsupported, last, shaders); + fragment.spirv = module({(5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, generated, (4u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationFlat}); + ValidateProvokingVertex(unsupported, last, shaders); + fragment.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, generated, (3u << 16u) | spv::OpDecorate, 2u, spv::DecorationFlat}); + refused(last); + fragment.spirv = module({(5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, generated, (4u << 16u) | spv::OpMemberDecorate, 1u, 1u, spv::DecorationFlat}); + refused(last); + } + for (const std::uint32_t passed : {static_cast(spv::BuiltInLayer), static_cast(spv::BuiltInViewportIndex), static_cast(spv::BuiltInPrimitiveId)}) { + fragment.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, passed, (3u << 16u) | spv::OpDecorate, 1u, spv::DecorationFlat}); + refused(last); + fragment.spirv = module({(5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, passed, (4u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationFlat}); + refused(last); + } + ShaderRecompiler::RecompileResult vertex{}; + vertex.fragmentParameters = {{0, 0, true, true, true, true, true}}; + vertex.spirv = module({(3u << 16u) | spv::OpDecorate, 1u, spv::DecorationFlat}); + fragment.fragmentParameters = {smooth}; + fragment.spirv = module({}); + const std::array stages{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + ValidateProvokingVertex(unsupported, last, stages); + Context supported{}; + supported.provokingVertexLast = true; + for (const auto stage : {ShaderRecompiler::ShaderStage::Vertex, ShaderRecompiler::ShaderStage::TessellationEvaluation, ShaderRecompiler::ShaderStage::Geometry, ShaderRecompiler::ShaderStage::Mesh}) { + ShaderRecompiler::RecompileResult writer{}; + const std::array written{{{stage, &writer, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}}; + const auto refusedWrite = [&] { + expectFailure([&] { ValidateProvokingVertex(unsupported, last, written); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); + ValidateProvokingVertex(supported, last, written); + }; + for (const std::uint32_t selected : {static_cast(spv::BuiltInLayer), static_cast(spv::BuiltInViewportIndex)}) { + writer.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, selected}); + refusedWrite(); + writer.spirv = module({(5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, selected}); + refusedWrite(); + writer.spirv = module({(5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, selected, (4u << 16u) | spv::OpMemberDecorate, 1u, 1u, spv::DecorationPerPrimitiveEXT}); + refusedWrite(); + writer.spirv = module({(4u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationPerPrimitiveEXT, (5u << 16u) | spv::OpMemberDecorate, 1u, 0u, spv::DecorationBuiltIn, selected}); + ValidateProvokingVertex(unsupported, last, written); + writer.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, selected, (3u << 16u) | spv::OpDecorate, 1u, spv::DecorationPerPrimitiveEXT}); + ValidateProvokingVertex(unsupported, last, written); + } + writer.spirv = module({(4u << 16u) | spv::OpDecorate, 1u, spv::DecorationBuiltIn, spv::BuiltInPosition, (5u << 16u) | spv::OpMemberDecorate, 2u, 0u, spv::DecorationBuiltIn, spv::BuiltInPointSize}); + ValidateProvokingVertex(unsupported, last, written); + writer.spirv = std::vector{}; + refusedWrite(); + } + const std::array missing{{{ShaderRecompiler::ShaderStage::Fragment, nullptr, 0}}}; + expectFailure([&] { ValidateProvokingVertex(unsupported, last, missing); }, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled"); +} + void ProvokingVertexRenderPassTests() { using namespace AgcDriver::Graphics; const auto first = DecodeState(makeState()); @@ -11397,6 +11496,12 @@ int main(int argc, char** argv) { std::cerr << "descriptor pool reset failure did not terminate\n"; return 1; } + if (argc == 2 && std::string_view(argv[1]) == "--unobserved-provoking-vertex") { + UnobservedProvokingVertexTests(); + ProvokingVertexTests(); + std::cout << "Unobserved provoking vertex checks passed\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--covered-centroid-state") { multisampleTests(); std::cout << "Covered-centroid and multisample state checks passed\n"; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 475aa11fb0..a9e5829f8c 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -142,7 +142,7 @@ - [PA_CL_CLIP_CNTL](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `CLIP_DISABLE` (bit 16) and `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) are accepted without changing pipeline state: Vulkan clips primitives against viewport guard bands and clips `NoPerspective` outputs linearly; what clearing bit 24 or setting bit 16 changes on the console beyond standard clipping is undocumented. - [10_11_11 unorm color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export, rounded to nearest even like format-30 image stores, which is assumed to match the CB's conversion. - [11_11_10 float color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) render into R32_UINT attachments: the pixel shader packs each export toward zero like format-43 image stores (overflow to the largest finite value, negatives to zero, NaN kept), which is assumed to match the CB's conversion; only the standard (COMP_SWAP 0) and reversed (COMP_SWAP 2) component orders are accepted, putting red and blue in the 10-bit low field. That follows from the format's layout and from the standard swap Mesa pairs with COLOR_10_11_11 for R11G11B10_FLOAT (ac_formats.c); it was not measured on the console. -- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) is not measured on the console: bit 19 follows opengnm's `flatshade_first` and Vulkan's vertex selection; rect lists take flat inputs from the third vertex (first mode: first); a geometry program gets odd strip triangle i as (i+1, i, i+2) in both modes (an RX 6700 XT, first mode: (i, i+2, i+1)); in last mode its primitives take flat inputs and layer from the last index, per-vertex ones in first-mode order; a flat input without pass-through read only via `v_interp_mov_f32` P10 (P20) is the provoking vertex's (ps5-opengl, #1413; first (last) mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Else last mode throws without `VK_EXT_provoking_vertex`, for tessellation, geometry-program fans, explicit per-vertex flat inputs, and strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`; 16-bit interpolation unchecked). +- [PA_SU_SC_MODE_CNTL.PROVOKING_VTX_LAST](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver) is not measured on the console: bit 19 follows opengnm's `flatshade_first` and Vulkan's vertex selection; rect lists take flat inputs from the third vertex (first mode: first); a geometry program gets odd strip triangle i as (i+1, i, i+2) in both modes (an RX 6700 XT, first mode: (i, i+2, i+1)); in last mode its primitives take flat inputs and layer from the last index, per-vertex ones in first-mode order; a flat input without pass-through read only via `v_interp_mov_f32` P10 (P20) is the provoking vertex's (ps5-opengl, #1413; first (last) mode throws). Barycentric tessellation stages give each corner the last vertex's flat values. Without `VK_EXT_provoking_vertex`, last mode throws only when a pixel shader input could show which vertex provokes (a flat input, a P10 flat read, a per-vertex or custom per-vertex input, or a `Flat` or `PerVertexKHR` decoration in its SPIR-V, ignoring built-ins the pixel stage generates) or another stage's SPIR-V decorates a `Layer` or `ViewportIndex` built-in that is not `PerPrimitiveEXT`; otherwise the draw uses first mode, since the provoking vertex then selects nothing it reads or writes. With the extension, last mode throws for tessellation, geometry-program fans, explicit per-vertex flat inputs, and strips whose pixel shader reads a vertex slot (`v_interp_mov_f32`; 16-bit interpolation unchecked). - [Triangle strips through the barycentric tessellation stages](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver) - without `geometryShader`, a triangle strip without primitive restart is expanded on the CPU into one three-point patch per triangle in Vulkan's strip order: (i, i+1, i+2), and for odd triangles (i, i+2, i+1), or (i+1, i, i+2) with `PROVOKING_VTX_LAST`. That is the order the geometry stage receives, so both routes give the same weights and per-vertex values; which order the console uses for odd triangles is not measured. Indirect strips throw. - [Dual-source blending](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) takes the MRT1 export as the second source of the MRT0 blend, as RADV and radeonsi program it; what the CB does with other export or target layouts is unknown, so they throw. - [Blending into reversed-order color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 8_8_8_8 and 16_16_16_16 targets with `COMP_SWAP` 2 or 3 keep the exported alpha in their first channel and red or blue in their fourth, which Vulkan would blend with the color and the alpha equation respectively. The color block is taken to blend in shader export order and apply `COMP_SWAP` afterwards: the alpha equation and every source-alpha factor use the exported alpha, and the destination alpha is the first channel. This follows the `CB_TARGET_MASK` text of the AMD [SI](https://xorg.freedesktop.org/docs/AMD/old/SI_3D_registers.pdf) (p. 229) and [CIK](https://xorg.freedesktop.org/docs/AMD/old/CIK_3D_registers_v2.pdf) (p. 238) register references ("the order of components after blending and before COMP_SWAP is applied"). RADV builds `CB_BLENDn_CONTROL` without regard to the swap ([radv_pipeline_graphics.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_pipeline_graphics.c#L650-L709)) and renders R4G4B4A4, B4G4R4A4, R5G5B5A1 and B5G5R5A1 with `SWAP_STD_REV` and `SWAP_ALT_REV` ([vk_format.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/vulkan/util/vk_format.c#L43-L48), [ac_formats.c](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/common/ac_formats.c#L609-L624)). Mesa CI's Navi10 (gfx1010) CTS run ([job 112381553](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381553), [112381554](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/112381554)) passes all 400 `blend.format..states` cases of each of those formats, including separate alpha and destination-alpha factors. It is not measured on gfx1013 or on 8_8_8_8 and 16_16_16_16 targets. Factor pairs Vulkan applies per channel are used directly. Otherwise the pixel shader only copies values: it writes the exported alpha, each channel's own export or the constants 0 and 1 into a second color at location 0 index 1 (and 1 into a channel of the first color whose source factor is ONE, moving its export into the factor), and the blend forms every factor, `1 -` and product from those copies through `SRC1_COLOR`/`SRC1_ALPHA` factors. The blend unit therefore rounds them as it rounds a plain Vulkan blend of the same export; Vulkan does not require a second-source color to be converted like the first, but on an RTX 5080 and lavapipe every reversed-order case of the execution test is bit-identical to the same draw into a `SWAP_STD` target, which the test checks. Factor combinations that copies cannot express (for example `ONE_MINUS_SRC_ALPHA` in the alpha equation with `SRC_COLOR` in the color equation), sRGB, SNORM and integer targets, constant and second-source factors, destination-alpha and `SRC_ALPHA_SATURATE` factors in the color equation, different color and alpha operations, MIN or MAX with factors other than ONE, other color exports when the blend needs the second color, and devices without `dualSrcBlend` throw. Gran Turismo 7 (PPSA01317) blends `CB_BLEND0_CONTROL` 0x65010504 into such a target (#4782). From 83caf5f9973a47aa94190b5a809c57c522776fec Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 10:46:40 -0300 Subject: [PATCH 231/318] docs(libkernel): say which sceKernelOpenEventFlag null-output answers were measured Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libkernel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index dce2237b6e..d959dc8c8f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -59,7 +59,7 @@ - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware. Cancellation follows the [POSIX condition wait contract](https://pubs.opengroup.org/onlinepubs/9799919799/functions/pthread_cond_clockwait.html): the original mutex and recursive depth are restored before cleanup, and a canceled waiter forwards a wake to the remaining waiters. The host condition state is retained across mutex reacquisition so the guest can destroy and free its condition after notification; cancellation ordering and signal replacement are not measured on a console -- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, where the console faulted; with a name it cannot open, both return `SCE_KERNEL_ERROR_ESRCH`. +- [sceKernelOpenEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - opens only flags this process created with attribute 0x100. On the console, other processes' 0x100 flags, such as the shell's `SceShellCoreUtilAppFocus`, can be opened too. They do not exist here, so opening them returns `SCE_KERNEL_ERROR_ESRCH`. A null output with the name of an openable flag returns `SCE_KERNEL_ERROR_EFAULT`, the host's answer to the console's fault there, not a measured code. With a plain flag's name it returns `SCE_KERNEL_ERROR_ESRCH`, as measured; with a missing name it also returns `SCE_KERNEL_ERROR_ESRCH`, which was not measured. - [sceKernelCreateEventFlag](../../../core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp) (libkernel) - when a call has both an invalid attribute and a name of 32 bytes or more, `SCE_KERNEL_ERROR_EINVAL` is returned, because the attribute checks run before the name length check. The console was only measured with long names and valid attributes (0x20 and 0x120), so which error it returns first is not known. Event flag handles are host addresses here, while the console returns small IDs. - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [scePthreadAttr setters and getters](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs. `scePthreadAttrGetschedpolicy` and `scePthreadAttrGetinheritsched` were not measured and follow [FreeBSD 11.4 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L216-L262); a null output of `scePthreadAttrGetinheritsched` throws, since libthr writes through it unchecked From ebf606ed01057514a950b887f13233ff5107da5d Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Fri, 23 Oct 2026 09:07:47 -0300 Subject: [PATCH 232/318] fix(libSceJobManager): batch compute completion in caller storage --- .../tests/execution/JobManagerCompute.cpp | 121 +++++++++++--- core/libs/prx/libSceJobManager/CMakeLists.txt | 10 ++ .../prx/libSceJobManager/ComputeProgress.hpp | 31 ++++ core/libs/prx/libSceJobManager/Export.cpp | 149 +++++++++++++----- core/libs/tests/JobManagerComputeProgress.cpp | 27 ++++ docs/dev/debt/libSceJobManager.md | 5 +- 6 files changed, 287 insertions(+), 56 deletions(-) create mode 100644 core/libs/prx/libSceJobManager/ComputeProgress.hpp create mode 100644 core/libs/tests/JobManagerComputeProgress.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp index c149aa41a9..d557f4e4c5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp @@ -1,5 +1,6 @@ #include "VulkanTestDevice.hpp" #include "SceTypes.hpp" +#include "prx/libc/include/GuestArena.hpp" #include #include #include @@ -11,6 +12,7 @@ #include #include #include +#include extern "C" void AgcDriverShutdown_nid_postfix(); @@ -60,6 +62,9 @@ struct SequenceFactory { }; extern "C" { +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void*); std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams*); int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(void*, void*, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint64_t, const ConfigParams*); @@ -94,10 +99,10 @@ alignas(256) volatile std::uint32_t Output[4]{}; alignas(256) std::array OutputDescriptor{}; alignas(32) std::array IndirectArguments{1, 1, 1, 0, 0, 0, 0, 0}; alignas(16) std::array Specials{}; -alignas(16) std::array SpecialsWithoutDispatch{}; +alignas(16) std::array SpecialsDefaultModifier{}; std::array Registers{}; alignas(16) std::array Shader{}; -alignas(16) std::array ShaderWithoutDispatch{}; +alignas(16) std::array ShaderDefaultModifier{}; std::uint32_t Low(const volatile void* pointer) { return static_cast(reinterpret_cast(pointer)); @@ -115,8 +120,8 @@ void BuildShader() { {0x242, Low(OutputDescriptor.data())}, {0x243, High(OutputDescriptor.data())}}}; Specials[4] = 1; const ShaderRegister* registers = Registers.data(); - for (auto* shader : {&Shader, &ShaderWithoutDispatch}) { - const std::uint32_t* specials = shader == &Shader ? Specials.data() : SpecialsWithoutDispatch.data(); + for (auto* shader : {&Shader, &ShaderDefaultModifier}) { + const std::uint32_t* specials = shader == &Shader ? Specials.data() : SpecialsDefaultModifier.data(); std::memcpy(shader->data() + 0x20, ®isters, sizeof(registers)); std::memcpy(shader->data() + 0x28, &specials, sizeof(specials)); (*shader)[0x5c] = static_cast(Registers.size()); @@ -144,10 +149,10 @@ void APS5_VABI Nothing(void*, std::uint32_t) { void SizeCase() { Require(SizeFor(0, 0, false) == 0x10d0 && SizeFor(0, 0, true) == 0x14c0, "size without compute sequences"); - Require(SizeFor(1, 1, false) == 0x24fd8 && SizeFor(1, 1, true) == 0x253c8, "size with one compute sequence"); - Require(SizeFor(2, 3, false) == 0x48e20 && SizeFor(2, 3, true) == 0x49210, "size with two compute sequences"); - Require(SizeFor(3, 7, false) == 0x6cb68, "size with three compute sequences"); - Require(SizeFor(1, 3, true) == 0x491c8, "size with two queues for one compute sequence"); + Require(SizeFor(1, 1, false) == 0x29fd8 && SizeFor(1, 1, true) == 0x2a3c8, "size with one compute sequence"); + Require(SizeFor(2, 3, false) == 0x52de0 && SizeFor(2, 3, true) == 0x531d0, "size with two compute sequences"); + Require(SizeFor(3, 7, false) == 0x7bb28, "size with three compute sequences"); + Require(SizeFor(1, 3, true) == 0x53188, "size with two queues for one compute sequence"); Require(SizeFor(2, 1, true) == 0, "size with fewer queues than compute sequences"); } @@ -192,24 +197,30 @@ void RunCase() { Require(Output[0] == 0x22220002u, "the indirect compute dispatch had not run when the wait returned: " + std::to_string(Output[0])); Value[0] = 0x33330003u; - Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the dispatch of a shader without the dispatch bit failed"); - Require(sequence->Wait() == 0, "the wait after a shader without the dispatch bit failed"); - Require(Output[0] == 0x22220002u, "a shader without the dispatch bit ran"); + Require(sequence->DispatchCompute(ShaderDefaultModifier.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the dispatch with a default modifier failed"); + Require(sequence->Wait() == 0, "the wait after a default modifier dispatch failed"); + Require(Output[0] == 0x33330003u, "a default modifier dispatch did not run"); + Value[0] = 0x33330004u; + Require(sequence->DispatchComputeIndirect(ShaderDefaultModifier.data(), Table.data(), IndirectArguments.data(), nullptr) == 0, "the indirect dispatch with a default modifier failed"); + Require(sequence->Wait() == 0, "the wait after an indirect default modifier dispatch failed"); + Require(Output[0] == 0x33330004u, "an indirect default modifier dispatch did not run"); + + Value[0] = 0x33330005u; Require(sequence->Dispatch(BlockingJob, nullptr, 1, "blocking") == 0, "the blocking job was not dispatched"); Require(sequence->SyncPoint() == 0, "the sync point failed"); Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the compute dispatch behind a sync point failed"); std::this_thread::sleep_for(std::chrono::milliseconds(200)); - Require(Output[0] == 0x22220002u, "a compute dispatch ran before the CPU job ahead of its sync point finished"); + Require(Output[0] == 0x33330004u, "a compute dispatch ran before the CPU job ahead of its sync point finished"); Released = true; Require(sequence->SyncPoint() == 0, "the second sync point failed"); Require(sequence->Dispatch(ReadOutputJob, nullptr, 1, "reader") == 0, "the reading job was not dispatched"); Require(sequence->Wait() == 0, "the wait after the ordered dispatches failed"); - Require(Output[0] == 0x33330003u, "the compute dispatch behind a sync point did not run"); - Require(SeenByJob == 0x33330003u, "a CPU job behind a sync point ran before the compute dispatch ahead of it finished"); + Require(Output[0] == 0x33330005u, "the compute dispatch behind a sync point did not run"); + Require(SeenByJob == 0x33330005u, "a CPU job behind a sync point ran before the compute dispatch ahead of it finished"); Value[0] = 0x44440004u; - Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the leading dispatch failed"); + Require(sequence->DispatchCompute(ShaderDefaultModifier.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the leading dispatch failed"); Require(sequence->SyncPoint() == 0, "the sync point between compute dispatches failed"); Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "behind") == 0, "the compute dispatch behind a compute dispatch failed"); Require(sequence->Wait() == 0, "the wait after chained compute dispatches failed"); @@ -229,16 +240,83 @@ void RunCase() { ::operator delete(memory, std::align_val_t{256}); } +void StorageCase(bool directMemory) { + constexpr std::uint64_t mask = (1u << 1) | (1u << 5) | (1u << 8); + const MemorySizeQueryParams query{1, 2, 0, 0, 32, 8, 4, 2, mask}; + const auto size = RequiredSize(&query); + const auto allocationSize = (size + 0xffff) & ~std::uint64_t{0xffff}; + std::int64_t physical = 0; + void* memory = nullptr; + if (directMemory) { + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, allocationSize, 0x10000, 0, &physical) == 0, "direct work area allocation failed"); + Require(sceKernelMapDirectMemory(&memory, allocationSize, 0x33, 0, physical, 0x10000) == 0, "direct work area mapping failed"); + } else { + memory = ::operator new(allocationSize, std::align_val_t{256}); + } + { + const GuestArena::HostWrite destination(memory, allocationSize); + std::memset(memory, 0, allocationSize); + std::memset(static_cast(memory) + size, 0xa5, allocationSize - size); + } + alignas(64) unsigned char manager[0x100]{}; + Construct(manager); + Require(Initialize(manager, memory, size, 2, 0, 2, mask, nullptr) == 0, "storage manager initialization failed"); + auto* factory = Factories(manager, 0); + std::array sequences{}; + for (auto& sequence : sequences) Require(factory->CreateCompute(&sequence, 0, 16, 4, 2, nullptr) == 0, "storage sequence creation failed"); + for (std::uint32_t round = 0; round < 3; ++round) { + for (auto* sequence : sequences) { + Value[0] = 0x56780000u + round; + for (int dispatch = 0; dispatch < 8; ++dispatch) + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "batch") == 0, "batched dispatch failed"); + Require(sequence->Wait() == 0, "batched wait failed"); + Require(Output[0] == Value[0], "batched work was not visible after wait"); + Require(sequence->Wait() == 0, "empty wait after a batch failed"); + } + const auto* words = static_cast(memory); + const auto base = reinterpret_cast(memory); + std::vector labels; + for (std::size_t index = 0; index + 8 <= size / 4; ++index) { + if (words[index] != 0xc0064900u || (words[index + 2] & 0xe7000000u) != 0x22000000u) continue; + const auto address = words[index + 3] | (std::uint64_t{words[index + 4]} << 32); + Require(address >= base && address + 8 <= base + size && (address & 7) == 0, "a completion label is outside the caller's work area"); + Require(words[index + 5] == 8 * (round + 1) && words[index + 6] == 0, "a completion did not publish the batched 32-bit count"); + Require(*reinterpret_cast(address) == words[index + 5], "a published batch was not retired"); + labels.push_back(address); + } + Require(labels.size() == 2 && labels[0] != labels[1], "each queue must store one batch completion in the caller's work area"); + } + for (auto* sequence : sequences) Require(factory->DestroyCompute(sequence) == 0, "storage sequence destruction failed"); + for (auto& sequence : sequences) { + Require(factory->CreateCompute(&sequence, 0, 16, 4, 2, nullptr) == 0, "storage sequence reuse failed"); + Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "dispatch after sequence reuse failed"); + Require(factory->DestroyCompute(sequence) == 0, "destroy did not wait for reused sequence work"); + } + Require(Shutdown(manager) == 0, "storage manager shutdown failed"); + const auto* bytes = static_cast(memory); + for (auto offset = size; offset < allocationSize; ++offset) + Require(bytes[offset] == 0xa5, "the manager wrote beyond its supplied work area"); + if (directMemory) Require(sceKernelReleaseDirectMemory(physical, allocationSize) == 0, "direct work area release failed"); + else ::operator delete(memory, std::align_val_t{256}); } -int main() { +} + +int RunJobManagerCase(const char* name) { try { + Require(std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0 || std::strcmp(name, "execution") == 0 || std::strcmp(name, "storage") == 0, "unknown compute fixture"); +#ifndef ANYPS5_JOB_MANAGER_REFERENCE auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; device.reset(); +#endif BuildShader(); - SizeCase(); - RunCase(); + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0) SizeCase(); + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "execution") == 0) RunCase(); + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "storage") == 0) { + StorageCase(false); + StorageCase(true); + } AgcDriverShutdown_nid_postfix(); std::cout << "job manager compute test passed\n"; return 0; @@ -252,3 +330,10 @@ int main() { return 1; } } + +#ifndef ANYPS5_JOB_MANAGER_REFERENCE +int main(int argc, char** argv) { + if (argc > 2) return 2; + return RunJobManagerCase(argc == 2 ? argv[1] : "compute"); +} +#endif diff --git a/core/libs/prx/libSceJobManager/CMakeLists.txt b/core/libs/prx/libSceJobManager/CMakeLists.txt index 8539bbd0c2..17c2c3d178 100644 --- a/core/libs/prx/libSceJobManager/CMakeLists.txt +++ b/core/libs/prx/libSceJobManager/CMakeLists.txt @@ -4,6 +4,16 @@ target_link_libraries(libSceJobManager PRIVATE libSceAgc libSceAgcDriver) configure_windows_unwind(libSceJobManager) if(BUILD_TESTING) + add_test_executable(job_manager_compute_progress_tests ${LIBS_INCLUDE_DIR}/tests/JobManagerComputeProgress.cpp) + target_include_directories(job_manager_compute_progress_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(job_manager_compute_progress_tests PRIVATE libc) + configure_windows_unwind(job_manager_compute_progress_tests) + add_test(NAME guest_job_manager_compute_progress COMMAND job_manager_compute_progress_tests) + set_tests_properties(guest_job_manager_compute_progress PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_job_manager_compute_progress PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_job_manager_tests ${LIBS_INCLUDE_DIR}/tests/GuestJobManager.cpp) target_include_directories(guest_job_manager_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_job_manager_tests PRIVATE libSceJobManager libSceAgc libSceAgcDriver libkernel libc) diff --git a/core/libs/prx/libSceJobManager/ComputeProgress.hpp b/core/libs/prx/libSceJobManager/ComputeProgress.hpp new file mode 100644 index 0000000000..aadd732245 --- /dev/null +++ b/core/libs/prx/libSceJobManager/ComputeProgress.hpp @@ -0,0 +1,31 @@ +#ifndef CORE_LIBS_PRX_LIBSCEJOBMANAGER_COMPUTEPROGRESS_HPP +#define CORE_LIBS_PRX_LIBSCEJOBMANAGER_COMPUTEPROGRESS_HPP + +#include +#include + +namespace JobManager { + +class ComputeProgress { +public: + std::uint32_t Publish(std::uint32_t count) { + total += count; + return total; + } + + std::uint32_t Collect(std::uint64_t value) { + const auto current = static_cast(value); + const std::uint32_t finished = current - seen; + if (finished > std::uint32_t{total - seen}) throw std::runtime_error("libSceJobManager: a compute label advanced beyond its published work"); + seen = current; + return finished; + } + +private: + std::uint32_t total = 0; + std::uint32_t seen = 0; +}; + +} + +#endif diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 155a95220d..b591aed225 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -12,7 +12,9 @@ #include #include "SceTypes.hpp" +#include "ComputeProgress.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" extern "C" { int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); @@ -41,6 +43,9 @@ std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf); std::uint32_t* APS5_VABI sceAgcCbDispatch(CommandBuffer* buf, std::uint32_t threadGroupX, std::uint32_t threadGroupY, std::uint32_t threadGroupZ, std::uint32_t modifier); std::uint32_t* APS5_VABI sceAgcAcbDispatchIndirect(CommandBuffer* buf, const volatile void* indirectArguments, std::uint32_t modifier); std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer* buf, std::uint8_t action, std::uint16_t gcrControl, std::uint8_t dst, std::uint8_t cachePolicy, const volatile Label* address, std::uint8_t dataSelect, std::uint64_t data, std::uint16_t gdsOffset, std::uint16_t gdsSize, std::uint8_t interrupt, std::uint32_t interruptContextId); +std::uint32_t* APS5_VABI sceAgcAcbWaitRegMem(CommandBuffer* buf, std::uint8_t size, std::uint8_t compareFunction, std::uint8_t cachePolicy, const volatile void* address, std::uint64_t reference, std::uint64_t mask, std::uint32_t pollCycles); +std::uint32_t* APS5_VABI sceAgcAcbRewind(CommandBuffer* buf, std::uint32_t initialState); +std::uint32_t* APS5_VABI sceAgcAcbWriteData(CommandBuffer* buf, std::uint8_t dst, std::uint8_t cachePolicy, std::uint64_t address, const void* data, std::uint32_t numDwords, std::uint8_t increment, std::uint8_t writeConfirm); } namespace { @@ -70,7 +75,6 @@ constexpr std::uint64_t ThreadRecordBytes = 0x48; constexpr std::uint64_t SequenceBytes = 0x240; constexpr std::uint64_t NodeBytes = 72; constexpr std::uint64_t ComputeContextBytes = 0x168; -constexpr std::uint64_t ComputeCommandBytes = 0x3ff * 104 + 0xff * 148 + 0x3f * 32; constexpr std::uint32_t FirstComputeQueue = 0x20; constexpr std::uint32_t UserDataRegister = 0x240; constexpr int UserEventFilter = -11; @@ -115,6 +119,32 @@ std::uint64_t AlignUp(std::uint64_t value, std::uint64_t alignment) { return (value + alignment - 1) & ~(alignment - 1); } +std::uint64_t ComputeCommandBytes() { + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const ShaderRegister shaderRegister{}; + const std::array userData{}; + bool encoded = sceAgcAcbAcquireMem(&buffer, 0x20380, nullptr, 0, 0x190) != nullptr; + encoded = encoded && sceAgcCbSetShRegistersDirect(&buffer, &shaderRegister, 1) != nullptr; + encoded = encoded && sceAgcCbSetShRegisterRangeDirect(&buffer, UserDataRegister, userData.data(), 2) != nullptr; + encoded = encoded && sceAgcAcbPushMarker(&buffer, "1234567890123456789012345678901", 0) != nullptr; + encoded = encoded && sceAgcCbDispatch(&buffer, 1, 1, 1, 0) != nullptr; + encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr; + const auto dispatchBytes = static_cast(buffer.cursor_up - words.data()) * 4; + buffer.cursor_up = words.data(); + encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0, 1, 3, nullptr, 1, 0, 0, 0, 0, 0) != nullptr; + encoded = encoded && sceAgcAcbWaitRegMem(&buffer, 0, 3, 3, nullptr, 0, ~std::uint64_t{0}, 0x190) != nullptr; + encoded = encoded && sceAgcAcbRewind(&buffer, 1) != nullptr; + encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0x200, 1, 3, nullptr, 1, 0, 0, 0, 0, 0) != nullptr; + encoded = encoded && sceAgcAcbWriteData(&buffer, 2, 3, 0, userData.data(), 1, 1, 0) != nullptr; + const auto syncBytes = static_cast(buffer.cursor_up - words.data()) * 4; + buffer.cursor_up = words.data(); + encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0x200, 1, 3, nullptr, 1, 0, 0, 0, 2, 0) != nullptr; + const auto waitBytes = static_cast(buffer.cursor_up - words.data()) * 4; + if (!encoded || dispatchBytes > 255 || syncBytes > 255 || waitBytes > 255) throw std::runtime_error("libSceJobManager: compute command size measurement failed"); + return MaxDispatches * dispatchBytes + MaxSyncPoints * syncBytes + MaxWaits * waitBytes; +} + struct Manager; class Sequence; @@ -177,6 +207,7 @@ public: void Advance(); void DispatchCompleted(std::uint32_t invocations); void CollectCompute(); + void SetComputeStorage(std::uint32_t* commands, std::size_t words, Label* completion); bool Busy() const { return outstanding != 0; } Manager* manager = nullptr; @@ -190,6 +221,7 @@ private: int WaitLocked(std::unique_lock& guard); int AppendCompute(Item item); void SubmitCompute(const Item& item); + void FlushCompute(); Counters limits; Counters submitted; @@ -200,11 +232,13 @@ private: std::size_t cursor = 0; std::deque items; std::condition_variable passed; - Label label{}; - std::uint64_t labelSeen = 0; - std::uint64_t labelTotal = 0; + Label* label = nullptr; + JobManager::ComputeProgress progress; + std::uint32_t pendingCompute = 0; bool batchStarted = false; - std::deque> commands; + std::uint32_t* commandBegin = nullptr; + std::size_t commandWords = 0; + std::size_t commandUsed = 0; }; class Factory final : public ISequenceFactory { @@ -267,7 +301,7 @@ bool Sequence::Open(Manager* owner, const Counters& limit, std::uint32_t runQueu outstanding = 0; cursor = 0; items.clear(); - commands.clear(); + commandUsed = 0; batchStarted = false; if (!TakeNode(*owner)) return false; open = true; @@ -294,6 +328,7 @@ void Sequence::Advance() { SubmitCompute(item); continue; } + FlushCompute(); if (outstanding != 0) return; if (item.type == ItemType::SyncPoint) { ++completed.syncPoints; @@ -304,7 +339,7 @@ void Sequence::Advance() { const auto retired = static_cast(cursor); items.erase(items.begin(), items.begin() + static_cast(retired + 1)); cursor = 0; - commands.clear(); + commandUsed = 0; batchStarted = false; if (error == 0) { submitted.invocations -= completed.invocations; @@ -353,31 +388,57 @@ void Sequence::SubmitCompute(const Item& item) { std::memcpy(&modifier, specials + 0x10, sizeof(modifier)); const auto userData = reinterpret_cast(item.argument); const std::array userDataWords{static_cast(userData), static_cast(userData >> 32)}; - auto& words = commands.emplace_back(64 + 3 * registerCount + item.name.size() / 4); - CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; - bool encoded = sceAgcAcbAcquireMem(&buffer, 0x20380u | (batchStarted ? 0u : 1u), nullptr, 0, 0x190) != nullptr; - encoded = encoded && sceAgcCbSetShRegistersDirect(&buffer, registers, registerCount) != nullptr; - encoded = encoded && sceAgcCbSetShRegisterRangeDirect(&buffer, UserDataRegister, userDataWords.data(), 2) != nullptr; - if (item.named) encoded = encoded && sceAgcAcbPushMarker(&buffer, item.name.c_str(), 0) != nullptr; - if ((modifier & 1) != 0) { + const auto capacity = std::min(commandWords - commandUsed, 64 + 3 * registerCount + item.name.size() / 4); + auto* const words = commandBegin + commandUsed; + CommandBuffer buffer{words, words + capacity, words, words + capacity, nullptr, nullptr, 0}; + { + const GuestArena::HostWrite destination(words, capacity * 4); + bool encoded = sceAgcAcbAcquireMem(&buffer, 0x20380u | (batchStarted ? 0u : 1u), nullptr, 0, 0x190) != nullptr; + encoded = encoded && sceAgcCbSetShRegistersDirect(&buffer, registers, registerCount) != nullptr; + encoded = encoded && sceAgcCbSetShRegisterRangeDirect(&buffer, UserDataRegister, userDataWords.data(), 2) != nullptr; + if (item.named) encoded = encoded && sceAgcAcbPushMarker(&buffer, item.name.c_str(), 0) != nullptr; if (item.type == ItemType::Compute) { encoded = encoded && sceAgcCbDispatch(&buffer, item.groups[0], item.groups[1], item.groups[2], modifier) != nullptr; } else { encoded = encoded && sceAgcAcbDispatchIndirect(&buffer, item.indirectArguments, modifier) != nullptr; } + if (item.named) encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr; + if (!encoded) throw std::runtime_error("libSceJobManager: a compute dispatch did not fit its command buffer"); } - if (item.named) encoded = encoded && sceAgcAcbPopMarker(&buffer) != nullptr; - encoded = encoded && sceAgcCbReleaseMem(&buffer, 0x2f, 0x620, 0, 0, &label, 1, ++labelTotal, 0, 0, 2, 0) != nullptr; - if (!encoded) throw std::runtime_error("libSceJobManager: a compute dispatch did not fit its command buffer"); batchStarted = true; - const Packet packet{words.data(), static_cast(buffer.cursor_up - words.data()), 0, {}}; + ++pendingCompute; + const Packet packet{words, static_cast(buffer.cursor_up - words), 0, {}}; + commandUsed += packet.dw_num; if (sceAgcDriverSubmitAcb(queue, &packet) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverSubmitAcb failed"); } +void Sequence::SetComputeStorage(std::uint32_t* commands, std::size_t words, Label* completion) { + commandBegin = commands; + commandWords = words; + label = completion; + const GuestArena::HostWrite destination(label, sizeof(*label)); + label->value = 0; +} + +void Sequence::FlushCompute() { + if (pendingCompute == 0) return; + if (commandWords - commandUsed < 8) throw std::runtime_error("libSceJobManager: a compute completion did not fit its command buffer"); + auto* const words = commandBegin + commandUsed; + CommandBuffer buffer{words, words + 8, words, words + 8, nullptr, nullptr, 0}; + { + const GuestArena::HostWrite destination(words, 32); + if (sceAgcCbReleaseMem(&buffer, 0x2f, 0x200, 1, 3, label, 1, progress.Publish(pendingCompute), 0, 0, 2, 0) == nullptr) { + throw std::runtime_error("libSceJobManager: a compute completion could not be encoded"); + } + } + pendingCompute = 0; + const Packet packet{words, static_cast(buffer.cursor_up - words), 0, {}}; + commandUsed += packet.dw_num; + if (sceAgcDriverSubmitAcb(queue, &packet) != 0) throw std::runtime_error("libSceJobManager: sceAgcDriverSubmitAcb failed for a completion"); +} + void Sequence::CollectCompute() { - const std::uint64_t value = label.value; - const auto finished = static_cast(value - labelSeen); - labelSeen = value; + const auto finished = progress.Collect(label->value); if (finished == 0) return; completed.dispatches += finished; outstanding -= finished; @@ -440,7 +501,7 @@ int Sequence::WaitAndClose(std::uint32_t index) { const auto remaining = static_cast(std::count_if(items.begin(), items.end(), [](const Item& item) { return item.type != ItemType::Wait; })); ReturnNodes(*manager, remaining + 1); items.clear(); - commands.clear(); + commandUsed = 0; cursor = 0; outstanding = 0; open = false; @@ -559,18 +620,28 @@ void* APS5_VABI WorkerEntry(void* argument) { return nullptr; } -std::uint32_t NodeCount(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeQueues, bool* fits) { +struct WorkLayout { + std::uint64_t commands = 0; + std::uint64_t commandBytes = 0; + std::uint64_t labels = 0; + std::uint32_t nodes = 0; +}; + +WorkLayout LayoutFor(std::uint64_t memory, std::uint64_t size, std::uint32_t workers, std::uint32_t sequences, std::uint32_t computeQueues) { + WorkLayout layout; const std::uint64_t end = memory + size; std::uint64_t address = AlignUp(memory + StateEnd, 8) + workers * ThreadRecordBytes; for (const std::uint64_t bytes : {std::uint64_t{0x100}, std::uint64_t{0x80}, std::uint64_t{0x80}, std::uint64_t{0x80}}) address = AlignUp(address, 64) + bytes; address = AlignUp(address, 64) + (sequences + computeQueues) * SequenceBytes; if (computeQueues != 0) { - address = AlignUp(address + ComputeContextBytes, 0x100) + AlignUp(ComputeCommandBytes * computeQueues, 8); - address += AlignUp(4 * std::uint64_t{computeQueues}, 8) + 8 * std::uint64_t{computeQueues}; + layout.commandBytes = ComputeCommandBytes(); + layout.commands = AlignUp(address + ComputeContextBytes, 0x100); + layout.labels = AlignUp(layout.commands + layout.commandBytes * computeQueues + 4 * std::uint64_t{computeQueues}, 8); + address = layout.labels + 8 * std::uint64_t{computeQueues}; } address = AlignUp(address, 64); - *fits = address <= end; - return *fits ? static_cast(((end - address) / NodeBytes) & 0xffff) : 0; + if (address <= end) layout.nodes = static_cast(((end - address) / NodeBytes) & 0xffff); + return layout; } int ValidateWorkers(const ConfigParams& config, std::uint32_t workers) { @@ -605,7 +676,7 @@ void StartWorkers(Manager& manager, const ConfigParams& config, std::uint32_t wo } } -void StartCompute(Manager& manager, std::uint32_t computeSequences, std::uint64_t computeQueueMask, std::uint32_t priority, std::uint32_t affinity) { +void StartCompute(Manager& manager, const WorkLayout& layout, std::uint32_t computeSequences, std::uint64_t computeQueueMask, std::uint32_t priority, std::uint32_t affinity) { if (sceKernelCreateEqueue(&manager.computeEvents, "SceJobManagerComputeEventQueue") != 0) throw std::runtime_error("libSceJobManager: sceKernelCreateEqueue failed"); for (std::uint64_t mask = computeQueueMask; mask != 0; mask &= mask - 1) { const std::uint32_t queue = FirstComputeQueue + static_cast(std::countr_zero(mask)); @@ -616,6 +687,8 @@ void StartCompute(Manager& manager, std::uint32_t computeSequences, std::uint64_ for (std::uint32_t index = 0; index < computeSequences; ++index) { manager.computeSequences[index].compute = true; manager.computeSequences[index].queue = manager.computeQueues[index]; + manager.computeSequences[index].SetComputeStorage(reinterpret_cast(layout.commands + index * layout.commandBytes), + static_cast(layout.commandBytes / 4), reinterpret_cast(layout.labels + index * sizeof(Label))); } manager.freeComputeSequences = computeSequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << computeSequences) - 1; const KernelSchedParam schedule{static_cast(priority)}; @@ -642,15 +715,17 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke } if (size < StateOffset) return ManagerMemoryTooSmall; if (size < StateEnd) throw std::runtime_error("sce::Job::JobManager::initialize: a memory block smaller than 0x2d8 bytes is not supported"); - std::memset(static_cast(memory) + StateOffset, 0, StateEnd - StateOffset); - const BlockHeader header{0x4d4a, static_cast(StateOffset), 0x05000001, static_cast(size), 0}; - std::memcpy(memory, &header, sizeof(header)); + { + const GuestArena::HostWrite destination(memory, StateEnd); + std::memset(static_cast(memory) + StateOffset, 0, StateEnd - StateOffset); + const BlockHeader header{0x4d4a, static_cast(StateOffset), 0x05000001, static_cast(size), 0}; + std::memcpy(memory, &header, sizeof(header)); + } if (config != nullptr && config->version > 1) return ManagerInvalidArgument; const ConfigParams& settings = config != nullptr ? *config : DefaultConfig; const auto computeQueues = computeSequences != 0 ? static_cast(std::popcount(computeQueueMask)) : 0; - bool fits = false; - const std::uint32_t nodes = NodeCount(base, size, workers, sequences, computeQueues, &fits); - if (!fits || nodes == 0) return ManagerMemoryTooSmall; + const auto layout = LayoutFor(base, size, workers, sequences, computeQueues); + if (layout.nodes == 0) return ManagerMemoryTooSmall; if (const int invalid = ValidateWorkers(settings, workers); invalid != 0) return invalid; const bool ownHandlerSettings = (settings.version & 1) != 0; const std::uint32_t handlerPriority = ownHandlerSettings ? settings.computePriority : 0x100; @@ -658,7 +733,7 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke if (computeSequences != 0 && (handlerPriority < 0x100 || handlerPriority > 0x2ff || handlerAffinity == 0 || handlerAffinity > 0x1fff)) return ManagerInvalidArgument; auto* manager = new Manager; manager->running = true; - manager->freeNodes = nodes - 1; + manager->freeNodes = layout.nodes - 1; manager->freeSequences = sequences == MaxSequences ? ~std::uint32_t{0} : (std::uint32_t{1} << sequences) - 1; manager->sequences = std::vector(sequences); for (std::uint32_t priority = 0; priority < 2; ++priority) { @@ -667,7 +742,7 @@ int Initialize(void* self, void* memory, std::uint64_t size, std::uint32_t worke manager->unifiedFactories[priority].manager = manager; manager->unifiedFactories[priority].priority = priority; } - if (computeSequences != 0) StartCompute(*manager, computeSequences, computeQueueMask, handlerPriority, handlerAffinity); + if (computeSequences != 0) StartCompute(*manager, layout, computeSequences, computeQueueMask, handlerPriority, handlerAffinity); Implementation(self) = manager; StartWorkers(*manager, settings, workers); return 0; @@ -724,7 +799,7 @@ std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKN if (computeSequences != 0) { const auto queues = static_cast(std::popcount(params->computeQueueMask)); bytes = (bytes + (params->sequences + queues) * SequenceBytes + 0x267) & ~std::uint64_t{0xff}; - bytes = (bytes + ComputeCommandBytes * queues + 3) & ~std::uint64_t{3}; + bytes = (bytes + ComputeCommandBytes() * queues + 3) & ~std::uint64_t{3}; bytes = (bytes + 4 * queues + 7) & ~std::uint64_t{7}; bytes += 8 * queues; } else { diff --git a/core/libs/tests/JobManagerComputeProgress.cpp b/core/libs/tests/JobManagerComputeProgress.cpp new file mode 100644 index 0000000000..fb0d90da1c --- /dev/null +++ b/core/libs/tests/JobManagerComputeProgress.cpp @@ -0,0 +1,27 @@ +#include "prx/libSceJobManager/ComputeProgress.hpp" +#include +#include +#include + +int main() { + try { + JobManager::ComputeProgress progress; + if (progress.Publish(0xfffffffd) != 0xfffffffd || progress.Collect(0xfffffffc) != 0xfffffffc) + throw std::runtime_error("partial retirement before wrap failed"); + if (progress.Publish(5) != 2 || progress.Collect(2) != 6 || progress.Collect(2) != 0) + throw std::runtime_error("batched completion across wrap failed"); + if (progress.Publish(9) != 11 || progress.Collect(5) != 3 || progress.Collect(11) != 6) + throw std::runtime_error("partial retirement after wrap failed"); + bool rejected = false; + try { + progress.Collect(12); + } catch (const std::runtime_error&) { + rejected = true; + } + if (!rejected || progress.Collect(11) != 0) throw std::runtime_error("an invalid completion changed retirement state"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/docs/dev/debt/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index 123977e935..184e91e23b 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -2,4 +2,7 @@ ### Functional -- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) (libSceJobManager) is written from the disassembly of the copies PPSA01474 and PPSA36082 ship in `sce_module`, and `guest_job_manager_tests` passes against both of those modules as well: the memory size formula, the error codes, sync point ordering, the per-sequence limits and the priority order are the original's. Compute sequences submit through `sceAgcDriverSubmitAcb` on the queue their mask bit selects and are checked the same way by `agc_driver_job_manager_compute_tests`: results, ordering against CPU dispatches and sync points, limits and sizes match the two modules. The command stream is simpler than the original's: every compute dispatch is submitted with its own label write and interrupt, where the original batches the label write and rewinds commands it encoded ahead, and the command buffers and labels live on the host heap. The per-queue command size in `calculateRequiredMemorySize` is a constant (104, 148 and 32 bytes per dispatch, sync point and wait), which is what the original measures by running the builders of this project's libSceAgc. A compute dispatch on a CPU sequence throws (the original does not check), and a failing `sceAgcDriverAddEqEvent` throws. `shutdown` throws while a compute sequence has work in flight, a sequence's label total is written as 32 bits, and since the label is host memory no GPU import covers, the driver drains recorded work before each label write. The manager's state lives on the host heap, with only the block header written to the title's memory; the original keeps everything in that block, and a block smaller than 0x2d8 bytes throws here. The worker sleep policy, the spin count and the persistent thread count are validated and stored but change nothing, a factory priority above 1 throws, `shutdown` throws while a thread is blocked in a sequence wait, and using a sequence after its `destroy` throws. `initialize` returns 0 where the original returns the result of its last import (`BG26hBGiNlw`, not resolved to a name), the `sceRazorCpuJobManager*` profiler calls are not made, and a worker thread that cannot be created or configured throws where the original does not check. The order of the sequence and factory virtual functions is read from the modules' vtables only, since no title that calls them has been run +- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) submits commands as work becomes runnable rather than encoding ahead with the rewind protocol recovered from PPSA06888's module (offsets 0x2210 and 0x2650). Commands are recycled after a completed wait; exhausting the bounded command area throws. Native recovery from command exhaustion and console timing equivalence remain unverified. +- The [manager](../../../core/libs/prx/libSceJobManager/Export.cpp) keeps scheduling state and nodes on the host heap; only the header, compute commands and completion labels occupy the caller's block. Blocks smaller than 0x2d8 bytes throw. The worker spin duration and persistent thread count are validated but do not govern scheduling. +- [Sequence and manager operations](../../../core/libs/prx/libSceJobManager/Export.cpp) throw on a compute dispatch on a CPU sequence, a failing `sceAgcDriverAddEqEvent`, a factory priority above 1, and use of a destroyed sequence. `shutdown` throws while compute work is in flight or a caller is blocked in a sequence wait. Worker creation and attribute failures throw where the original does not check. +- [`initialize`](../../../core/libs/prx/libSceJobManager/Export.cpp) returns 0 instead of the result of the original's final import (`BG26hBGiNlw`, unresolved). The `sceRazorCpuJobManager*` profiler calls are not made. Sequence and factory virtual-function order is recovered from module vtables; no calling title has been run. From 0891ca275e37a23f4de9971eb6e6d7617f25d623 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Fri, 23 Oct 2026 10:51:24 -0300 Subject: [PATCH 233/318] test(job-manager): verify native compute command capacity --- .../tests/execution/JobManagerCompute.cpp | 47 ++++++++++++++++++- core/libs/prx/libSceJobManager/Export.cpp | 6 +-- docs/dev/debt/libSceJobManager.md | 2 +- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp index d557f4e4c5..8d5f9f14c7 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/JobManagerCompute.cpp @@ -1,6 +1,7 @@ #include "VulkanTestDevice.hpp" #include "SceTypes.hpp" #include "prx/libc/include/GuestArena.hpp" +#include #include #include #include @@ -240,6 +241,49 @@ void RunCase() { ::operator delete(memory, std::align_val_t{256}); } +void CapacityCase() { + std::array registers; + registers.fill(Registers[2]); + std::copy(Registers.begin(), Registers.end(), registers.begin()); + auto shader = Shader; + const ShaderRegister* data = registers.data(); + std::memcpy(shader.data() + 0x20, &data, sizeof(data)); + shader[0x5c] = static_cast(registers.size()); + const std::string marker(65535, 'm'); + const MemorySizeQueryParams query{1, 1, 0, 0, 2, 0, 1, 1, 1}; + const auto size = RequiredSize(&query); + const auto allocationSize = (size + 255) & ~std::uint64_t{255}; + auto* memory = static_cast(::operator new(allocationSize, std::align_val_t{256})); + std::memset(memory, 0, allocationSize); + std::memset(memory + size, 0xa5, allocationSize - size); + alignas(64) unsigned char manager[0x100]{}; + Construct(manager); + Require(Initialize(manager, memory, size, 1, 0, 1, 1, nullptr) == 0, "capacity manager initialization failed"); + auto* factory = Factories(manager, 0); + Sequence* sequence = nullptr; + Require(factory->CreateCompute(&sequence, 0, 2, 0, 1, nullptr) == 0, "capacity sequence creation failed"); + for (std::uint32_t round = 0; round < 2; ++round) { + Value[0] = 0xabcd0000u + round; + for (int dispatch = 0; dispatch < 2; ++dispatch) + Require(sequence->DispatchCompute(shader.data(), Table.data(), 1, 1, 1, marker.c_str()) == 0, "maximum register and marker dispatch failed"); + Require(sequence->Wait() == 0, "maximum packet wait failed"); + Require(Output[0] == Value[0], "maximum packet dispatch did not finish"); + const auto* words = reinterpret_cast(memory); + std::uint32_t markers = 0; + for (std::size_t offset = 0; offset + 0x4001 <= size / 4; ++offset) { + if (words[offset] != 0xffff102cu) continue; + Require(std::memcmp(words + offset + 1, marker.c_str(), marker.size() + 1) == 0, "maximum marker was truncated"); + ++markers; + } + Require(markers == 2, "the maximum markers were not stored in the command area"); + } + Require(factory->DestroyCompute(sequence) == 0, "capacity sequence destruction failed"); + Require(Shutdown(manager) == 0, "capacity manager shutdown failed"); + for (auto offset = size; offset < allocationSize; ++offset) + Require(memory[offset] == 0xa5, "maximum packets escaped the supplied work area"); + ::operator delete(memory, std::align_val_t{256}); +} + void StorageCase(bool directMemory) { constexpr std::uint64_t mask = (1u << 1) | (1u << 5) | (1u << 8); const MemorySizeQueryParams query{1, 2, 0, 0, 32, 8, 4, 2, mask}; @@ -304,7 +348,7 @@ void StorageCase(bool directMemory) { int RunJobManagerCase(const char* name) { try { - Require(std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0 || std::strcmp(name, "execution") == 0 || std::strcmp(name, "storage") == 0, "unknown compute fixture"); + Require(std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0 || std::strcmp(name, "execution") == 0 || std::strcmp(name, "storage") == 0 || std::strcmp(name, "capacity") == 0, "unknown compute fixture"); #ifndef ANYPS5_JOB_MANAGER_REFERENCE auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; @@ -313,6 +357,7 @@ int RunJobManagerCase(const char* name) { BuildShader(); if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "size") == 0) SizeCase(); if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "execution") == 0) RunCase(); + if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "capacity") == 0) CapacityCase(); if (std::strcmp(name, "compute") == 0 || std::strcmp(name, "storage") == 0) { StorageCase(false); StorageCase(true); diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index b591aed225..a215aeed0f 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -119,7 +119,7 @@ std::uint64_t AlignUp(std::uint64_t value, std::uint64_t alignment) { return (value + alignment - 1) & ~(alignment - 1); } -std::uint64_t ComputeCommandBytes() { +std::uint64_t ComputeCommandBudget() { std::array words{}; CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; const ShaderRegister shaderRegister{}; @@ -634,7 +634,7 @@ WorkLayout LayoutFor(std::uint64_t memory, std::uint64_t size, std::uint32_t wor for (const std::uint64_t bytes : {std::uint64_t{0x100}, std::uint64_t{0x80}, std::uint64_t{0x80}, std::uint64_t{0x80}}) address = AlignUp(address, 64) + bytes; address = AlignUp(address, 64) + (sequences + computeQueues) * SequenceBytes; if (computeQueues != 0) { - layout.commandBytes = ComputeCommandBytes(); + layout.commandBytes = ComputeCommandBudget(); layout.commands = AlignUp(address + ComputeContextBytes, 0x100); layout.labels = AlignUp(layout.commands + layout.commandBytes * computeQueues + 4 * std::uint64_t{computeQueues}, 8); address = layout.labels + 8 * std::uint64_t{computeQueues}; @@ -799,7 +799,7 @@ std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKN if (computeSequences != 0) { const auto queues = static_cast(std::popcount(params->computeQueueMask)); bytes = (bytes + (params->sequences + queues) * SequenceBytes + 0x267) & ~std::uint64_t{0xff}; - bytes = (bytes + ComputeCommandBytes() * queues + 3) & ~std::uint64_t{3}; + bytes = (bytes + ComputeCommandBudget() * queues + 3) & ~std::uint64_t{3}; bytes = (bytes + 4 * queues + 7) & ~std::uint64_t{7}; bytes += 8 * queues; } else { diff --git a/docs/dev/debt/libSceJobManager.md b/docs/dev/debt/libSceJobManager.md index 184e91e23b..5390f41870 100644 --- a/docs/dev/debt/libSceJobManager.md +++ b/docs/dev/debt/libSceJobManager.md @@ -2,7 +2,7 @@ ### Functional -- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) submits commands as work becomes runnable rather than encoding ahead with the rewind protocol recovered from PPSA06888's module (offsets 0x2210 and 0x2650). Commands are recycled after a completed wait; exhausting the bounded command area throws. Native recovery from command exhaustion and console timing equivalence remain unverified. +- [sce::Job::JobManager](../../../core/libs/prx/libSceJobManager/Export.cpp) submits commands as work becomes runnable rather than encoding ahead with the rewind protocol recovered from PPSA06888's module (offsets 0x2210 and 0x2650). Commands are recycled after a completed wait. Command capacity is a separate bound from the dispatch count: the native size query measures one register and a 31-character marker, then its exhaustion callback at 0x29d0 traps instead of growing storage. The fallback throws. Original-module execution with the host AGC builders accepts two dispatches with 255 registers and 65,535-character markers, then traps on a third. Console timing equivalence remains unverified. - The [manager](../../../core/libs/prx/libSceJobManager/Export.cpp) keeps scheduling state and nodes on the host heap; only the header, compute commands and completion labels occupy the caller's block. Blocks smaller than 0x2d8 bytes throw. The worker spin duration and persistent thread count are validated but do not govern scheduling. - [Sequence and manager operations](../../../core/libs/prx/libSceJobManager/Export.cpp) throw on a compute dispatch on a CPU sequence, a failing `sceAgcDriverAddEqEvent`, a factory priority above 1, and use of a destroyed sequence. `shutdown` throws while compute work is in flight or a caller is blocked in a sequence wait. Worker creation and attribute failures throw where the original does not check. - [`initialize`](../../../core/libs/prx/libSceJobManager/Export.cpp) returns 0 instead of the result of the original's final import (`BG26hBGiNlw`, unresolved). The `sceRazorCpuJobManager*` profiler calls are not made. Sequence and factory virtual-function order is recovered from module vtables; no calling title has been run. From 87bc1aa04dd2fc2428c75413ee2e4e3e10694fe8 Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Fri, 23 Oct 2026 19:48:23 +0600 Subject: [PATCH 234/318] fix(tools): recognize spaced environment reader calls --- tools/env_reference.py | 6 ++- .../test_check_conventions_env_reference.py | 21 +++++++++ tools/tests/test_env_reference.py | 45 +++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/tools/env_reference.py b/tools/env_reference.py index b7277df3bb..f6bb761417 100644 --- a/tools/env_reference.py +++ b/tools/env_reference.py @@ -12,11 +12,12 @@ SKIP = {"build", "build-linux", "build-relinker", "3rdparty", ".git"} INTEGER_READERS = {"kibSetting", "MaskFromEnvironment", "EnvInt", "configured", "environmentCount"} STRING_READERS = {"GetEnvironmentVariableA", "GetEnvironmentVariableW", "addWideString"} -NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?_?w?getenv\(") +NULL_TEST = re.compile(r"[!=]=\s*(nullptr|NULL|0)\b|!\s*(?:std::)?_?w?getenv\s*\(") INTEGER_TEST = re.compile(r"\b(atoi|strtol|strtoul|strtoll|strtoull|stoi|stol|stoul|from_chars|atoi64|_wtoi|_wtol|_wtoi64)\s*\(") GREP = (r'(getenv|_wgetenv|wgetenv|IsEnvironmentSet|kibSetting|MaskFromEnvironment|EnvInt|GetEnvironmentVariable[AW]' r'|configured|PipelineSwitch|environmentCount|addWideString)' r'\([[:space:]]*(u|L)?"(APS5|ANYPS5)_[A-Za-z0-9_]+"') +GREP = GREP.replace(r"\(", r"[[:space:]]*\(") def _name(prefixes): @@ -26,7 +27,7 @@ def _name(prefixes): @functools.lru_cache(maxsize=None) def forms(prefixes=PREFIXES): name = _name(prefixes) - return ( + readers = ( ("getenv", re.compile(r'(?:std::)?getenv\(\s*"(?P' + name + r')"\s*\)')), ("_wgetenv", re.compile(r'(?:std::)?_?wgetenv\(\s*L"(?P' + name + r')"\s*\)')), ("IsEnvironmentSet", re.compile(r'IsEnvironmentSet\(\s*"(?P' + name + r')"\s*\)')), @@ -40,6 +41,7 @@ def forms(prefixes=PREFIXES): ("GetEnvironmentVariableW", re.compile(r'GetEnvironmentVariableW\(\s*L"(?P' + name + r')"')), ("addWideString", re.compile(r'\baddWideString\(\s*u"(?P' + name + r')"\s*\)')), ) + return tuple((reader, re.compile(form.pattern.replace(r"\(", r"\s*\(", 1), form.flags)) for reader, form in readers) def line_reads(line, prefixes=PREFIXES): diff --git a/tools/tests/test_check_conventions_env_reference.py b/tools/tests/test_check_conventions_env_reference.py index 270218d686..d8fe70da63 100644 --- a/tools/tests/test_check_conventions_env_reference.py +++ b/tools/tests/test_check_conventions_env_reference.py @@ -85,6 +85,27 @@ class EnvReferenceDiffTests(unittest.TestCase): with self.subTest(directory=directory): self.assertEqual(self.run_case(base, head, target, directory), expected) + def test_a_new_spaced_read_is_reported_at_its_line(self): + base = {DOC: DOC_TEXT, SAMPLE: "int unrelated;\n"} + head = {SAMPLE: base[SAMPLE] + 'auto value = !std::getenv \t("APS5_NEW_READ");\n'} + self.assertEqual(self.run_case(base, head), [("env-reference", SAMPLE, 2, + f"APS5_NEW_READ is read here but has no row in {DOC}")]) + + def test_removing_a_compact_read_keeps_a_row_still_used_by_a_spaced_read(self): + other = "core/libs/prx/libSceExample/Other.cpp" + base = { + DOC: DOC_TEXT, + SAMPLE: 'auto a = std::getenv("APS5_DOCUMENTED");\n', + other: 'auto b = _wgetenv \t(L"APS5_DOCUMENTED");\n', + } + self.assertEqual(self.run_case(base, {SAMPLE: "int unrelated;\n"}), []) + + def test_a_removed_row_is_reported_when_a_spaced_read_remains(self): + base = {DOC: DOC_TEXT, SAMPLE: 'auto value = std::getenv \t("APS5_DOCUMENTED");\n'} + head = {DOC: "| `APS5_GONE` | presence |\n"} + self.assertEqual(self.run_case(base, head), [("env-reference", DOC, 0, + "APS5_DOCUMENTED is still read by the code but this pull request removes its row")]) + def test_only_a_name_the_pull_request_adds_is_reported_at_its_line(self): base = { DOC: DOC_TEXT, diff --git a/tools/tests/test_env_reference.py b/tools/tests/test_env_reference.py index f56a933b14..36020ce584 100644 --- a/tools/tests/test_env_reference.py +++ b/tools/tests/test_env_reference.py @@ -78,6 +78,51 @@ class EnvReferenceTests(unittest.TestCase): found = env_reference.reads([root / "core"]) self.assertIn("APS5_MULTILINE", found) + def test_whitespace_before_parentheses_preserves_names_kinds_and_defaults(self): + root = self.fixture() + source = root / "core" / "Sample.cpp" + expected = env_reference.reads([root / "core"]) + for separator in (" ", "\t", " \t "): + with self.subTest(separator=separator): + source.write_text(CODE.replace("(", separator + "("), encoding="utf-8") + self.assertEqual(env_reference.reads([root / "core"]), expected) + + def test_reader_and_parenthesis_on_separate_lines_keep_line_numbers_and_kinds(self): + root = self.fixture() + source = root / "core" / "Sample.cpp" + source.write_text('auto a = !std::getenv\n ("APS5_PRESENT");\n' + 'auto b = _wtoi(_wgetenv\n (L"ANYPS5_INTEGER"));\n', encoding="utf-8") + found = env_reference.reads([root / "core"]) + self.assertEqual(set(found), {"APS5_PRESENT", "ANYPS5_INTEGER"}) + self.assertEqual(found["APS5_PRESENT"], [(source.as_posix(), 1, "presence", "")]) + self.assertEqual(found["ANYPS5_INTEGER"], [(source.as_posix(), 3, "integer", "")]) + + def test_main_check_rejects_an_undocumented_spaced_read(self): + root = self.fixture() + (root / "core" / "Sample.cpp").write_text('auto value = !std::getenv \t("APS5_UNDOCUMENTED");\n', encoding="utf-8") + doc = root / "ENVIRONMENT.md" + doc.write_text("", encoding="utf-8") + with contextlib.redirect_stdout(io.StringIO()) as out: + code = env_reference.main(["--check", "--root", str(root / "core"), "--doc", str(doc)]) + self.assertEqual(code, 1) + self.assertIn("APS5_UNDOCUMENTED is read", out.getvalue()) + self.assertNotIn("all documented", out.getvalue()) + + def test_negated_spaced_getenv_is_a_presence_read(self): + root = self.fixture() + (root / "core" / "Sample.cpp").write_text('auto a = !std::getenv ("APS5_PRESENT");\n' + 'auto b = !_wgetenv\t(L"ANYPS5_PRESENT");\n', encoding="utf-8") + found = env_reference.reads([root / "core"]) + self.assertEqual(set(found), {"APS5_PRESENT", "ANYPS5_PRESENT"}) + self.assertEqual([sites[0][2] for sites in found.values()], ["presence", "presence"]) + + def test_spacing_does_not_change_literal_parentheses_in_a_custom_prefix(self): + for separator in ("", " ", "\t"): + with self.subTest(separator=separator): + source = f'getenv{separator}("CUSTOM(NAME"); getenv{separator}("CUSTOM (WRONG");' + found = env_reference.line_reads(source, ("CUSTOM(",)) + self.assertEqual([match.group("name") for _, match in found], ["CUSTOM(NAME"]) + def test_multiline_reads_keep_line_numbers_and_do_not_duplicate(self): root = self.fixture() source = root / "core" / "Sample.cpp" From 98737e21751ac31365e6c2f72463b8b923d1f36b Mon Sep 17 00:00:00 2001 From: Kovalev Vladislav Date: Fri, 23 Oct 2026 20:29:54 +0600 Subject: [PATCH 235/318] fix(tools): measure Windows test batches after quoting --- tools/select_tests.py | 7 +++-- tools/tests/test_select_tests.py | 45 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index 80005e556d..78f6fc375d 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -146,11 +146,14 @@ def relative(path, root): def chunks(names): batch, size = [], 0 for name in names: - if batch and size + len(name) + 1 > BATCH: + length = len(name) + 1 + if sys.platform == "win32": + length = len(subprocess.list2cmdline([name]).encode("utf-16-le", "surrogatepass")) // 2 + 1 + if batch and size + length > BATCH: yield batch batch, size = [], 0 batch.append(name) - size += len(name) + 1 + size += length if batch: yield batch diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index b89ba0904a..38fa425aa6 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -339,6 +339,51 @@ class CaseInsensitivePathTests(unittest.TestCase): self.assertIsNone(build.repo(os.path.join(os.sep, "WORK", "SOURCE", "BUILD", "Generated.h"))) +class ChunkTests(unittest.TestCase): + def test_windows_batches_fit_the_serialized_utf16_budget(self): + cases = ( + (20000, ["\U0001f600" * 50 + f"/{index}.obj" for index in range(400)]), + (64, ['space name\\', 'embedded"quote', "\t", "", 'both \\" slash\\'] * 20), + (64, ["\ud800" * 10, "\U0001f600" * 10] * 20), + ) + for budget, names in cases: + with self.subTest(budget=budget, first=names[0]), mock.patch.object(select_tests.sys, "platform", "win32"), \ + mock.patch.object(select_tests, "BATCH", budget): + batches = list(select_tests.chunks(iter(names))) + self.assertEqual([name for batch in batches for name in batch], names) + self.assertGreater(len(batches), 1) + for batch in batches: + self.assertTrue(batch) + command = subprocess.list2cmdline(batch) + self.assertLessEqual(len(command.encode("utf-16-le", "surrogatepass")) // 2 + 1, budget) + + def test_windows_batch_keeps_an_exact_boundary(self): + with mock.patch.object(select_tests.sys, "platform", "win32"), mock.patch.object(select_tests, "BATCH", 8): + self.assertEqual(list(select_tests.chunks(["a b", "c", "d"])), [["a b", "c"], ["d"]]) + + def test_other_platforms_keep_the_existing_character_budget(self): + names = ["\U0001f600" * 3, "abc", "\udcff"] + for platform in ("linux", "darwin"): + with self.subTest(platform=platform), mock.patch.object(select_tests.sys, "platform", platform), \ + mock.patch.object(select_tests, "BATCH", 8): + self.assertEqual(list(select_tests.chunks(iter(names))), [names[:2], names[2:]]) + + def test_empty_input_and_an_oversized_single_argument_are_preserved(self): + for platform in ("win32", "linux"): + with self.subTest(platform=platform), mock.patch.object(select_tests.sys, "platform", platform), \ + mock.patch.object(select_tests, "BATCH", 4): + self.assertEqual(list(select_tests.chunks(iter([]))), []) + self.assertEqual(list(select_tests.chunks(["long target", "x"])), [["long target"], ["x"]]) + + @unittest.skipUnless(sys.platform == "win32", "needs the Windows command line limit") + def test_windows_batches_launch_without_losing_arguments(self): + names = ["\U0001f600" * 50 + f"/{index}.obj" for index in range(400)] + for batch in select_tests.chunks(names): + result = subprocess.run([sys.executable, "-I", "-c", "import json,sys; print(json.dumps(sys.argv[1:]))", *batch], + capture_output=True, text=True, check=True, timeout=5) + self.assertEqual(json.loads(result.stdout), batch) + + class PlanTests(unittest.TestCase): def test_unresolved_selected_command_keeps_unselected_build_targets(self): build = mock.Mock() From 3bcd89e10eb5e00c4251c6e5a60be6b00c979fff Mon Sep 17 00:00:00 2001 From: Santhosh Kumar Date: Sun, 18 Oct 2026 00:11:09 +0530 Subject: [PATCH 236/318] fix(agc): reclaim idle images safely during cache creation Forward new-image recovery permission through AllocateImageMemory while preserving host-memory fallback and allocated-type reporting. Reuse held cache locks only at safe construction points, retain caller-held sampled textures and pending storage results, and try resource-cache removal without waiting. Count released Vulkan allocations and saturate the reclaim reserve. Cover sampled/storage allocation failures, host fallback after image recovery, bounded attempts and overflow. Remove stale conflict markers and unrelated debt, and refresh the recorded validation. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 ++ .../Graphics/include/BufferPool.hpp | 8 +- .../Graphics/include/Context.hpp | 2 +- .../Graphics/include/ShaderResources.hpp | 4 +- .../Graphics/include/Texture.hpp | 2 +- .../Graphics/src/BufferPool.cpp | 16 +-- .../Graphics/src/ShaderResources.cpp | 113 ++++++++++-------- .../libSceAgcDriver/Graphics/src/Texture.cpp | 12 +- .../prx/libSceAgcDriver/tests/BufferPool.cpp | 18 ++- .../tests/execution/SampledCacheBudget.cpp | 83 ++++++++++++- .../tests/execution/StorageCacheBudget.cpp | 11 +- docs/dev/debt/libSceAgcDriver.md | 1 + docs/user/USAGE.md | 2 + 13 files changed, 203 insertions(+), 77 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 03b1a6a617..08f96fd7ad 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2786,6 +2786,14 @@ if(BUILD_TESTING) set_tests_properties(agc_sampled_cache_device_selection agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + foreach(imageRecoveryMode IN ITEMS sampled storage) + add_test(NAME agc_driver_recover_${imageRecoveryMode}_allocation COMMAND agc_driver_sampled_cache_budget_tests recover-${imageRecoveryMode}) + set_tests_properties(agc_driver_recover_${imageRecoveryMode}_allocation PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_driver_recover_${imageRecoveryMode}_allocation PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp index 5dcf54c727..871368f956 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/BufferPool.hpp @@ -66,9 +66,9 @@ public: // The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large. static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties); static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties); - VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr, bool newImage = false); static VkDeviceSize ImageBudget(VkDeviceSize deviceBudget); - VkDeviceMemory TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr); + VkDeviceMemory TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType = std::nullopt, std::uint32_t* allocatedType = nullptr, bool newImage = false); void PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; std::size_t RetainedImages(); std::optional Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); @@ -143,7 +143,7 @@ private: // The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once. static std::size_t MaxSlots(); std::size_t maxSlots; - VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes); + VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes, bool newImage); VkDevice device; PFN_vkUnmapMemory unmap; PFN_vkDestroyBuffer destroyBuffer; @@ -182,7 +182,7 @@ private: }; std::shared_ptr GetBufferPool(const Context& context); -VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType = nullptr); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType = nullptr, bool newImage = false); void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 4019bd9ff5..286666da03 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -194,7 +194,7 @@ struct Context { std::uint32_t settleFamily = 0; std::mutex* settleQueueMutex = nullptr; PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr; - VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes) = nullptr; + VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes, bool newImage) = nullptr; RenderCache* renderCache = nullptr; DrawQueue* drawQueue = nullptr; GraphicsPipelineCache* graphicsPipelines = nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index d090ebd057..0ed65026c6 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -30,7 +30,7 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); -std::uint64_t ReleaseTextureCaches(std::uint64_t bytes); +std::uint64_t ReleaseTextureCaches(std::uint64_t bytes, bool newImage = false); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); @@ -705,6 +705,7 @@ public: // meanwhile by another worker stays). void Remove(const Key& key, const ShaderResources* object = nullptr); void RemoveImage(const StorageTexture* image, std::vector>* evicted = nullptr); + bool TryRemoveImage(const StorageTexture* image, std::vector>* evicted = nullptr); // Moves the entry to the front of the LRU (a recipe hit uses its template without Find, and a // hot template must not age out under the recipes that depend on it); false when no entry. bool Touch(const Key& key); @@ -727,6 +728,7 @@ private: void noteEvicted(const Key& key); void noteReturn(const Key& key); void noteUse(const Key& key); + void removeImage(const StorageTexture* image, std::vector>* evicted); struct KeyHash { std::size_t operator()(const Key& key) const noexcept { constexpr std::uint64_t Prime = 0x9e3779b97f4a7c15ull; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 4dde83053f..e218054371 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -47,7 +47,7 @@ struct OwnedImage { OwnedImage(const OwnedImage&) = delete; OwnedImage& operator=(const OwnedImage&) = delete; ~OwnedImage(); - void Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation); + void Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, bool newImage = false); Context context; VkImage image; VkDeviceMemory memory; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 128970a898..715b75a6b4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -57,7 +57,7 @@ BufferPool::~BufferPool() { for (auto& [memory, block] : slabBlocks) freeBlock(*block); } -VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType, std::uint32_t* allocatedType) { +VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType, std::uint32_t* allocatedType, bool newImage) { if (allocatedType != nullptr) *allocatedType = allocation.memoryTypeIndex; VkDeviceMemory memory = VK_NULL_HANDLE; auto result = allocateMemory(device, &allocation, nullptr, &memory); @@ -75,7 +75,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation } } if (exhausted(result)) { - if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize)); + if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize), newImage); if (released != 0) { reclaimed += trim(); memory = VK_NULL_HANDLE; @@ -146,7 +146,7 @@ bool BufferPool::heapOverBudget() { return heapOver; } -VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType, std::uint32_t* allocatedType) { +VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional hostType, std::uint32_t* allocatedType, bool newImage) { const auto type = allocation.memoryTypeIndex; if (imagePooled(allocation.allocationSize, type)) { std::lock_guard lock(mutex); @@ -168,7 +168,7 @@ VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocatio } ++imageMisses; } - return AllocateMemory(allocation, operation, hostType, allocatedType); + return AllocateMemory(allocation, operation, hostType, allocatedType, newImage); } void BufferPool::PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { @@ -217,7 +217,9 @@ std::size_t BufferPool::RetainedImages() { } VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) { - return bytes + std::max(bytes, VkDeviceSize{256} << 20u); + const auto reserve = std::max(bytes, VkDeviceSize{256} << 20u); + const auto limit = std::numeric_limits::max(); + return bytes > limit - reserve ? limit : bytes + reserve; } bool BufferPool::SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom) { @@ -533,8 +535,8 @@ std::shared_ptr GetBufferPool(const Context& context) { return context.bufferPool; } -VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType) { - return GetBufferPool(context)->TakeImageMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex), allocatedType); +VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType, bool newImage) { + return GetBufferPool(context)->TakeImageMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex), allocatedType, newImage); } void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 4d56d0db54..09f1984c79 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -16,7 +16,6 @@ #include #include #include -#include #include #include #include @@ -163,6 +162,8 @@ TextureCache& Textures() { thread_local unsigned sampledCacheHeld = 0; thread_local unsigned storageCacheHeld = 0; +thread_local unsigned sampledCacheReclaimable = 0; +thread_local unsigned storageCacheReclaimable = 0; struct CacheHold { unsigned& depth; @@ -172,15 +173,6 @@ struct CacheHold { CacheHold& operator=(const CacheHold&) = delete; }; -bool AcquireCache(AgcDriver::Mutex& mutex) { - const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(20); - do { - if (mutex.try_lock()) return true; - std::this_thread::yield(); - } while (std::chrono::steady_clock::now() < deadline); - return false; -} - struct UncachedImages { AgcDriver::Mutex mutex; std::vector> images; @@ -919,14 +911,18 @@ std::shared_ptr cachedTexture(const Context& context, std::span(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration}; entry.committed = std::move(committed); entry.surfaceBytes = bytes; + const auto createTexture = [&](auto&&... args) { + const CacheHold reclaimable(sampledCacheReclaimable); + return std::make_shared(context, std::forward(args)...); + }; if (source != nullptr) { entry.source = source; entry.sourceVersion = source->Version(); if (depthCompare) { - entry.texture = std::make_shared(context, source, resource, components, Texture::DepthCopyTag{}); + entry.texture = createTexture(source, resource, components, Texture::DepthCopyTag{}); entry.texture->RefreshDepthCopy(); } else if (blockCopy) { - entry.texture = std::make_shared(context, source, resource, components, Texture::BlockCopyTag{}); + entry.texture = createTexture(source, resource, components, Texture::BlockCopyTag{}); entry.texture->RefreshDepthCopy(); } else if (IsRepackedTextureFormat(resource.format)) { entry.texture = std::make_shared(context, source, resource, components, Texture::RepackCopyTag{}); @@ -935,7 +931,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, source, resource, components, Texture::SrgbExpandTag{}); entry.texture->RefreshDepthCopy(); } else { - entry.texture = std::make_shared(context, source, resource, components); + entry.texture = createTexture(source, resource, components); } entry.accounted = source->Cached() ? 0u : heldBytes(*source); counters.fromStorage.fetch_add(1, std::memory_order_relaxed); @@ -951,7 +947,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span(resource.baseAddress), resource.width, resource.height, resource.format, static_cast(resource.tileMode), nonzero, entry.bytes.size() / 64); } - entry.texture = std::make_shared(context, *context.detiler, resource, components, entry.bytes, depthCompare); + entry.texture = createTexture(*context.detiler, resource, components, entry.bytes, depthCompare); entry.accounted = entry.texture->AllocationBytes(); constexpr std::size_t SnapshotCompareBytes = 256u << 10u; if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) std::vector().swap(entry.bytes); @@ -1179,7 +1175,12 @@ std::shared_ptr cachedStorageTexture(const Context& context, std } else { if (guestBytes == 0) guestBytes = DescribeSurface(resource).guestBytes; if (StorageTexture::FlushPending(resource.baseAddress, static_cast(guestBytes), nullptr, "storage image creation", PublishScope::None) && profile) start = LookupOutcomes::Add(LookupOutcomes::PendingFlush, start); - CachedStorageTexture entry{key, mip, StorageTexture::Create(context, *context.detiler, resource, mip)}; + std::shared_ptr created; + { + const CacheHold reclaimable(storageCacheReclaimable); + created = StorageTexture::Create(context, *context.detiler, resource, mip); + } + CachedStorageTexture entry{key, mip, std::move(created)}; static const bool keepNew = std::getenv("APS5_NO_KEEP_NEW_STORAGE") == nullptr; if (auto* recorder = Recorder::Active(); keepNew && recorder != nullptr && GuestMemory::GpuMutex().HeldByThisThread() && recorder->Recording()) recorder->Keep(entry.texture); entry.accounted = heldBytes(*entry.texture); @@ -1294,6 +1295,7 @@ void FlushCachedTextures(VkDevice device) { GuestMemory::AssertGpuLockHeld("FlushCachedTextures"); auto& cache = StorageTextures(); std::lock_guard lock(cache.mutex); + const CacheHold held(storageCacheHeld); for (const auto& entry : cache.entries) { if (entry.key.device == device) entry.texture->Flush(); } @@ -1304,6 +1306,7 @@ void ClearCachedTextures(VkDevice device) { auto& sampled = Textures(); { std::lock_guard lock(sampled.mutex); + const CacheHold held(sampledCacheHeld); for (auto it = sampled.entries.begin(); it != sampled.entries.end();) { if (it->key.device == device) eraseTexture(sampled, it++); else ++it; @@ -1313,6 +1316,7 @@ void ClearCachedTextures(VkDevice device) { std::vector> cleared; { std::lock_guard lock(storage.mutex); + const CacheHold held(storageCacheHeld); for (auto it = storage.entries.begin(); it != storage.entries.end();) { if (it->key.device != device) { ++it; @@ -1339,7 +1343,7 @@ void ClearCachedTextures(VkDevice device) { } } -std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { +std::uint64_t ReleaseTextureCaches(std::uint64_t bytes, bool newImage) { thread_local bool releasing = false; if (releasing) return 0; releasing = true; @@ -1355,63 +1359,67 @@ std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) { bool storageSkipped = false; { auto& cache = Textures(); - if (sampledCacheHeld != 0 || !AcquireCache(cache.mutex)) { + std::unique_lock lock(cache.mutex, std::defer_lock); + const bool inherited = sampledCacheHeld != 0; + if ((inherited && (!newImage || sampledCacheReclaimable == 0)) || (!inherited && !lock.try_lock())) { sampledSkipped = true; } else { - std::lock_guard lock(cache.mutex, std::adopt_lock); - dropUncachedViews(cache); - while (freed < bytes && !cache.entries.empty()) { - const auto back = std::prev(cache.entries.end()); - const auto accounted = back->accounted; - const bool last = back->texture.use_count() == 1; - eraseTexture(cache, back); + for (auto it = cache.entries.end(); it != cache.entries.begin() && freed < bytes;) { + --it; + if (it->texture.use_count() != 1 || (it->source != nullptr && it->source->Dirty())) continue; + const auto allocated = it->texture->AllocationBytes(); + const auto sourceBytes = it->source != nullptr ? it->source->AllocationBytes() : 0u; + const std::weak_ptr source = it->source; + const auto next = std::next(it); + eraseTexture(cache, it); ++cache.counts.evicted; ++sampledEntries; - if (last) freed += accounted; + freed += allocated; + if (source.expired()) freed += sourceBytes; + it = next; } } } if (freed < bytes) { auto& cache = StorageTextures(); std::unique_lock gpu(GuestMemory::GpuMutex(), std::defer_lock); - if (storageCacheHeld != 0 || !gpu.try_lock()) { + const bool inherited = storageCacheHeld != 0; + if ((inherited && (!newImage || storageCacheReclaimable == 0)) || !gpu.try_lock()) { storageSkipped = true; } else { try { while (freed < bytes) { - std::vector, std::uint64_t>> evicted; + std::shared_ptr image; { - if (!AcquireCache(cache.mutex)) { + std::unique_lock lock(cache.mutex, std::defer_lock); + if (!inherited && !lock.try_lock()) { storageSkipped = true; break; } - std::lock_guard lock(cache.mutex, std::adopt_lock); - std::uint64_t batch = 0; storageKept = 0; - for (auto it = cache.entries.end(); it != cache.entries.begin() && batch + freed < bytes;) { + for (auto it = cache.entries.end(); it != cache.entries.begin();) { --it; if (it->texture->Dirty()) { ++storageKept; continue; } - const auto accounted = it->accounted; - const auto next = std::next(it); - evicted.emplace_back(evictStorage(cache, it), accounted); - batch += accounted; - it = next; + image = evictStorage(cache, it); + break; } - if (evicted.empty()) break; + if (image == nullptr) break; } - for (auto& [image, accounted] : evicted) { - std::vector> resources; - SharedResourceCache().RemoveImage(image.get(), &resources); - if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { - for (auto& resource : resources) recorder->Keep(std::move(resource)); - } - resources.clear(); - ++storageEntries; - if (image.use_count() == 1) freed += accounted; - image.reset(); + std::vector> resources; + const bool removed = SharedResourceCache().TryRemoveImage(image.get(), &resources); + if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + for (auto& resource : resources) recorder->Keep(std::move(resource)); + } + resources.clear(); + ++storageEntries; + if (image.use_count() == 1) freed += image->AllocationBytes(); + image.reset(); + if (!removed) { + storageSkipped = true; + break; } } } catch (const std::exception& error) { @@ -3943,6 +3951,17 @@ void ResourceCache::Remove(const Key& key, const ShaderResources* object) { void ResourceCache::RemoveImage(const StorageTexture* image, std::vector>* evicted) { std::lock_guard lock(mutex); + removeImage(image, evicted); +} + +bool ResourceCache::TryRemoveImage(const StorageTexture* image, std::vector>* evicted) { + std::unique_lock lock(mutex, std::try_to_lock); + if (!lock.owns_lock()) return false; + removeImage(image, evicted); + return true; +} + +void ResourceCache::removeImage(const StorageTexture* image, std::vector>* evicted) { for (auto it = entries.begin(); it != entries.end();) { if (!it->second->ReadsImage(image)) { ++it; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 632ed7bb71..5060743d4a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -48,8 +48,8 @@ OwnedImage::~OwnedImage() { ReleaseImageMemory(context, memory, memoryBytes, memoryType); } -void OwnedImage::Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation) { - memory = AllocateImageMemory(device, allocation, mask, operation, &memoryType); +void OwnedImage::Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, bool newImage) { + memory = AllocateImageMemory(device, allocation, mask, operation, &memoryType, newImage); memoryBytes = allocation.allocationSize; context.bufferPool = device.bufferPool; } @@ -366,7 +366,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory texture", true); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); const VkImage uploadImage = image; @@ -701,7 +701,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory block copy", true); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory block copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -760,7 +760,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory depth copy", true); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory depth copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; @@ -1452,7 +1452,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler, allocation.allocationSize = requirements.size; memoryBytes = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture"); - memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture", &memoryType); + memory = AllocateImageMemory(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory storage texture", &memoryType, true); Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage"); } catch (...) { release(); diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index abdf8f0166..bf7d146519 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -675,6 +676,7 @@ struct ReclaimMock { VkDeviceSize externalBytes = 0; std::uint64_t requests = 0; VkDeviceSize asked = 0; + bool newImage = false; }; ReclaimMock reclaimMock; @@ -689,7 +691,8 @@ VkDeviceMemory ExternalAllocation(VkDeviceSize bytes) { return memory; } -VkDeviceSize mockCacheReclaim(VkDeviceSize bytes) { +VkDeviceSize mockCacheReclaim(VkDeviceSize bytes, bool newImage) { + Expect(newImage == reclaimMock.newImage, "an allocation used the wrong image-creation cache access"); ++reclaimMock.requests; reclaimMock.asked = bytes; if (reclaimMock.external == VK_NULL_HANDLE) return 0; @@ -701,6 +704,10 @@ VkDeviceSize mockCacheReclaim(VkDeviceSize bytes) { void CacheReclaimRecovery() { Expect(BufferPool::ReclaimTarget(4 * MiB) == 260 * MiB, "a 4 MiB request does not ask the caches for 256 MiB on top of itself"); Expect(BufferPool::ReclaimTarget(512 * MiB) == 1024 * MiB, "a 512 MiB request does not ask the caches for twice its size"); + const auto limit = std::numeric_limits::max(); + Expect(BufferPool::ReclaimTarget(limit / 2u) == limit - 1u, "the largest doubled request without overflow lost its reserve"); + Expect(BufferPool::ReclaimTarget(limit / 2u + 1u) == limit, "a reclaim target wrapped when twice the request overflowed"); + Expect(BufferPool::ReclaimTarget(limit) == limit, "the largest reclaim request wrapped to a smaller target"); constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT; mock = MockDevice{}; reclaimMock = ReclaimMock{}; @@ -775,18 +782,23 @@ void ImageCacheReclaim() { VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = 8 * MiB; allocation.memoryTypeIndex = 0; - const auto refuse = [&](std::initializer_list results) { + const auto refuse = [&](std::initializer_list results, bool newImage = false) { ExternalAllocation(4 * MiB); + reclaimMock.newImage = newImage; mock.results = results; const auto requests = reclaimMock.requests; - const auto memory = AllocateImageMemory(context, allocation, 3, "vkAllocateMemory texture"); + const auto before = mock.attempts; + const auto memory = AllocateImageMemory(context, allocation, 3, "vkAllocateMemory texture", newImage); Expect(reclaimMock.requests == requests + 1 && reclaimMock.external == VK_NULL_HANDLE, "a texture refused device memory did not release the texture caches before retrying"); + Expect(mock.attempts == before + results.size() + 1, "image recovery or host fallback exceeded its bounded attempts"); const auto type = mock.memoryTypes.at(memory); mockFreeMemory(VK_NULL_HANDLE, memory, nullptr); return type; }; Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY}) == 0, "a texture was placed in host memory although releasing the texture caches made room in device memory"); Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) == 1, "a texture still refused device memory after releasing the texture caches was not placed in host memory"); + Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY}, true) == 0, "a new image was placed in host memory before retrying after cache release"); + Expect(refuse({VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}, true) == 1, "a new image refused device memory after cache release lost its host fallback"); Expect(mock.liveBytes == 0, "a texture allocation leaked memory"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 46e3184ea6..93ef5d147f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" @@ -1006,6 +1007,78 @@ void storageMemoryTests(Context context) { std::printf("reported heap budget %llu MiB with %llu MiB in use: storage image cache budget %llu MiB\n", static_cast(ReportedBudget >> 20u), static_cast(PressedUsage >> 20u), static_cast(pressed >> 20u)); } +struct RecoveryProbe { + PFN_vkGetDeviceProcAddr deviceProc = nullptr; + std::uint64_t released = 0; + unsigned requests = 0; + unsigned failures = 0; + bool pressure = false; +}; + +RecoveryProbe recovery; + +VkDeviceSize ReclaimForTest(VkDeviceSize bytes, bool newImage) { + Require(newImage, "an image allocation did not identify itself to the recovery callback"); + ++recovery.requests; + Require(ReleaseTextureCaches(bytes, false) == 0, "a buffer allocation was allowed to reclaim the cache held by image creation"); + const auto released = ReleaseTextureCaches(bytes, newImage); + recovery.released += released; + return released; +} + +VKAPI_ATTR VkResult VKAPI_CALL AllocateForTest(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + if (recovery.pressure && recovery.released == 0) { + ++recovery.failures; + return VK_ERROR_OUT_OF_DEVICE_MEMORY; + } + return reinterpret_cast(recovery.deviceProc(device, "vkAllocateMemory"))(device, info, allocator, memory); +} + +VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL RecoveryProc(VkDevice device, const char* name) { + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(AllocateForTest); + return recovery.deviceProc(device, name); +} + +void recoveryTests(Context context, bool storage) { + recovery = RecoveryProbe{}; + recovery.deviceProc = context.deviceProc; + context.deviceProc = RecoveryProc; + context.cacheReclaim = ReclaimForTest; + context.bufferPool.reset(); + Block block(context); + if (storage) { + auto oldest = CachedStorageSurface(context, Resource(block.Surface(0))); + const auto allocated = oldest->AllocationBytes(); + std::weak_ptr released = oldest; + oldest.reset(); + auto pending = CachedStorageSurface(context, Resource(block.Surface(1))); + pending->MarkDirty(); + recovery.pressure = true; + auto made = CachedStorageSurface(context, Resource(block.Surface(2))); + recovery.pressure = false; + Require(made != nullptr && released.expired(), "storage allocation recovery did not release its own oldest clean image"); + Require(pending->Dirty() && StorageImageCached(context, pending.get()), "storage allocation recovery changed an image with pending results"); + Require(recovery.released == allocated, "storage allocation recovery counted memory still held by its caller"); + pending->Flush(); + Require(!pending->Dirty(), "pending storage results could not be stored after memory recovery"); + } else { + auto oldest = Sampled(context, block.Surface(0)); + Require(!oldest->ViewsStorageImage(), "the sampled recovery fixture needs a snapshot allocation"); + const auto allocated = oldest->AllocationBytes(); + std::weak_ptr released = oldest; + oldest.reset(); + auto held = Sampled(context, block.Surface(1)); + recovery.pressure = true; + auto made = Sampled(context, block.Surface(2)); + recovery.pressure = false; + Require(made != nullptr && released.expired(), "sampled allocation recovery did not release its own oldest unused texture"); + Require(Sampled(context, block.Surface(1)) == held, "sampled allocation recovery removed a texture still held by its caller"); + Require(recovery.released == allocated, "sampled allocation recovery counted memory still held by its caller"); + } + Require(recovery.requests == 1 && recovery.failures >= 1 && recovery.failures <= 2, "image allocation recovery was skipped or retried without a bound"); + std::printf("%s image allocation recovered after %u injected failures: %llu bytes released\n", storage ? "storage" : "sampled", recovery.failures, static_cast(recovery.released)); +} + } int main(int argc, char** argv) { @@ -1018,11 +1091,12 @@ int main(int argc, char** argv) { } const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; + const bool recover = mode == "recover-sampled" || mode == "recover-storage"; #ifdef _WIN32 - if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); + if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", recover ? "16" : "1"); if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "250"); #else - if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", "1", 1); + if (!memory) setenv("APS5_TEXTURE_CACHE_MIB", recover ? "16" : "1", 1); setenv("APS5_TEMPLATE_AUDIT_MS", "250", 0); #endif std::unique_ptr device; @@ -1082,6 +1156,11 @@ int main(int argc, char** argv) { std::puts("sampled texture memory budget tests passed"); return 0; } + if (recover) { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + recoveryTests(context, mode == "recover-storage"); + return 0; + } { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); reportedBudgetTests(context); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp index a3a273ff50..87090fa580 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp @@ -162,6 +162,7 @@ int main(int argc, char** argv) { Require(first != nullptr, "surface 0 has no cached storage image"); Require(AgcDriver::Graphics::SharedResourceCache().Size() != 0, "the storage cache workload did not populate the resource cache"); const auto held = std::max(first->AllocationBytes(), first->GuestBytes()); + const auto allocated = first->AllocationBytes(); const auto reused = static_cast(FixedBudget / held) + 1u; std::printf("texture cache budget %llu MiB; each 512x512 32_UINT surface holds %llu MiB; cyclic set of %u surfaces (%llu MiB)\n", static_cast(budget >> 20u), static_cast(held >> 20u), reused, static_cast((reused * held) >> 20u)); Require(budget >= reused * held, "the cyclic set exceeds the test cache budget"); @@ -183,8 +184,8 @@ int main(int argc, char** argv) { Require(cached == (surface >= 2u), "surface " + std::to_string(surface) + (cached ? " is still cached" : " was evicted") + " after " + std::to_string(capacity + 2u) + " surfaces under a budget of " + std::to_string(capacity) + " of them"); Require(cached == !images[surface].expired(), "surface " + std::to_string(surface) + (cached ? " is cached but its weak image expired" : " was evicted but its resource cache image is still alive")); } - const auto released = AgcDriver::Graphics::ReleaseTextureCaches(held); - Require(released >= held, "a release of one image's bytes freed " + std::to_string(released >> 20u) + " MiB, less than the " + std::to_string(held >> 20u) + " MiB of the oldest image"); + const auto released = AgcDriver::Graphics::ReleaseTextureCaches(allocated); + Require(released == allocated, "a release of one image's bytes counted " + std::to_string(released) + " bytes, not its " + std::to_string(allocated) + "-byte allocation"); Require(StorageTexture::FindLive(base + 2u * Stride, SurfaceBytes) == nullptr && images[2].expired(), "the oldest cached storage image survived a release of its bytes"); Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) != nullptr && !images[3].expired(), "a release of one image's bytes evicted more than the oldest image"); Require(StorageTexture::FindLive(base + (capacity + 1u) * Stride, SurfaceBytes) != nullptr, "a release of one image's bytes evicted the newest image"); @@ -194,11 +195,11 @@ int main(int argc, char** argv) { Require(Store(*device, base, 4) != nullptr, "surface 4 is not cached"); for (std::uint32_t surface = 5; surface < capacity + 2u; ++surface) Require(Store(*device, base, surface) != nullptr, "surface " + std::to_string(surface) + " is not cached"); } - Require(AgcDriver::Graphics::ReleaseTextureCaches(held) >= held, "a release with a dirty oldest image freed nothing"); + Require(AgcDriver::Graphics::ReleaseTextureCaches(allocated) == allocated, "a release with a dirty oldest image did not free one clean allocation"); Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) != nullptr, "a release took the dirty oldest image before a clean one"); Require(StorageTexture::FindLive(base + 4u * Stride, SurfaceBytes) == nullptr && images[4].expired(), "a release did not take the oldest clean image"); const auto rest = AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}); - Require(rest >= held * (capacity - 3u), "a release of everything freed " + std::to_string(rest >> 20u) + " MiB of the " + std::to_string((held * (capacity - 3u)) >> 20u) + " MiB cached"); + Require(rest == allocated * (capacity - 3u), "a release of everything counted " + std::to_string(rest) + " bytes, not the " + std::to_string(allocated * (capacity - 3u)) + " bytes allocated"); device->WaitIdle(); for (std::uint32_t surface = 0; surface < capacity + 2u; ++surface) { if (surface == 3u) continue; @@ -210,7 +211,7 @@ int main(int argc, char** argv) { } StorageTexture::FlushPending(base + 3u * Stride, static_cast(SurfaceBytes), nullptr, "test"); device->WaitIdle(); - Require(AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}) >= held, "a release did not take the image once its results were stored"); + Require(AgcDriver::Graphics::ReleaseTextureCaches(~std::uint64_t{0}) == allocated, "a release did not take the image once its results were stored"); Require(StorageTexture::FindLive(base + 3u * Stride, SurfaceBytes) == nullptr, "surface 3 survived a release after its results were stored"); Require(AgcDriver::Graphics::ReleaseTextureCaches(held) == 0, "a release from empty caches reported freed bytes"); } catch (...) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index a9e5829f8c..b573617dbf 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -69,6 +69,7 @@ - [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. +- [GPU allocation recovery](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - recovery may free too little and still throw: caller-held sampled images, pending storage results and recorder references stay alive. Budgets remain floors rather than hard device limits. Image and buffer allocation failures trim idle buffers, then sampled and clean storage cache entries. A held cache lock is reused only for a new image's allocation after lookup iterators are no longer used; hit refreshes and flushes skip that cache. Other cache, resource and device locks are tried without waiting. Fault-injection fixtures passed on Windows 11 (AMD RX 6700 XT, AMD Vulkan 26.8.1); the integrated image-allocation wrapper passed on Linux (AMD RX 9070 XT, RADV). Other hardware driver stacks are unverified. - [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images (at least the sampled textures alive) and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache and never falls below 2 GiB. The storage budget keeps no room for the part of that 2 GiB floor the sampled textures alive do not use: it gives room back as they grow, at its next read. As the heap's other use grows the sampled budget reaches its floor first, then the storage budget falls. Each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads, sampled textures included, can still exceed the reported budget. The eighth is the sampled formula's value reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond PPSA11386 on one 10 GB GPU and PPSA16064 on one 8 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. - [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index bf137f6dd6..3a08d85d7c 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -299,6 +299,8 @@ Besides the recompiled shaders, the shader cache directory holds `pipelines.jour ### Texture cache budget +Image and buffer memory allocations retry after releasing unused pooled buffers and reclaimable texture cache entries. Recovery preserves storage images with pending GPU results and resources retained by recorded work. `[bufferpool]` recovery and failure lines report the requested allocation, memory released and retry count. An allocation still fails if recovery cannot free enough memory. + Sampled textures and storage images stay in device memory in two caches that evict their least recently used entries when they grow past their budgets. The storage image budget is what the `VK_EXT_memory_budget` budget of the largest device-local heap leaves after the heap's use other than the cached storage images themselves and an eighth of the budget; it is capped at a quarter of that budget, at most a quarter of the heap's size or 2 GiB, whichever is larger, and never below 512 MiB. The sampled texture budget is what the reported budget leaves after the heap's use other than cached textures, a quarter of the heap's size (at least 2 GiB) and an eighth of the budget, and never below 2 GiB. As the heap's other use grows the sampled budget falls first, to that floor, then the storage budget; the storage budget also gives back what the sampled textures alive take as they grow. Each cache trims at its next insert. Both budgets are read again every second; `[gpu] sampled texture cache budget` lines report them at start-up and when either moves by more than a tenth, at most every 10 s. `APS5_TEXTURE_CACHE_MIB=` fixes both budgets at `n` MiB, and the lines are then not printed. A fixed budget above the automatic one can exceed what the card holds next to the rest of the frame. Allocations then go to host memory (`[gpu] ... placed in host memory` lines), where the GPU reads them over PCIe. PPSA16064 on an RTX 4060 8 GB, whose automatic storage image budget was 1797 MiB under the earlier rule, placed 868 and 878 MiB in host memory with `APS5_TEXTURE_CACHE_MIB=3072` and walked through its first level at 4.46 and 4.83 FPS; with the variable unset it placed none and ran the same walk at 6.48 and 6.07 FPS. Leave it unset unless a measurement on the card shows otherwise. From 737f6073ec01b1b71558b47844d47878a8018b16 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:09:17 -0300 Subject: [PATCH 237/318] test(agc): adapt recovery callback signatures --- core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp index bf7d146519..d1efb26289 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BufferPool.cpp @@ -788,7 +788,7 @@ void ImageCacheReclaim() { mock.results = results; const auto requests = reclaimMock.requests; const auto before = mock.attempts; - const auto memory = AllocateImageMemory(context, allocation, 3, "vkAllocateMemory texture", newImage); + const auto memory = AllocateImageMemory(context, allocation, 3, "vkAllocateMemory texture", nullptr, newImage); Expect(reclaimMock.requests == requests + 1 && reclaimMock.external == VK_NULL_HANDLE, "a texture refused device memory did not release the texture caches before retrying"); Expect(mock.attempts == before + results.size() + 1, "image recovery or host fallback exceeded its bounded attempts"); const auto type = mock.memoryTypes.at(memory); @@ -912,7 +912,7 @@ void DeviceBindingFailures() { VkDeviceMemory reclaimImage = VK_NULL_HANDLE; const Context* reclaimImageContext = nullptr; -VkDeviceSize mockImageReclaim(VkDeviceSize) { +VkDeviceSize mockImageReclaim(VkDeviceSize, bool) { if (reclaimImage == VK_NULL_HANDLE) return 0; ReleaseImageMemory(*reclaimImageContext, reclaimImage, 10 * MiB, 0); reclaimImage = VK_NULL_HANDLE; From 8e47a3b5d6c24fa48ecec3782f1e61fc7033ccff Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:21:12 -0300 Subject: [PATCH 238/318] test(agc): cover copied-image allocation recovery --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 2 +- .../tests/execution/SampledCacheBudget.cpp | 57 +++++++++++++++---- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 08f96fd7ad..52b265a546 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2786,7 +2786,7 @@ if(BUILD_TESTING) set_tests_properties(agc_sampled_cache_device_selection agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() - foreach(imageRecoveryMode IN ITEMS sampled storage) + foreach(imageRecoveryMode IN ITEMS sampled storage srgb repack) add_test(NAME agc_driver_recover_${imageRecoveryMode}_allocation COMMAND agc_driver_sampled_cache_budget_tests recover-${imageRecoveryMode}) set_tests_properties(agc_driver_recover_${imageRecoveryMode}_allocation PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 93ef5d147f..2fe3e34065 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -327,10 +327,10 @@ private: std::uint8_t* memory = nullptr; }; -std::array Descriptor(std::uint64_t address, std::uint32_t side = Side) { +std::array Descriptor(std::uint64_t address, std::uint32_t side = Side, std::uint32_t format = Format32UInt) { return { static_cast(address >> 8u), - static_cast((address >> 40u) & 0xffu) | (Format32UInt << 20u) | (((side - 1u) & 3u) << 30u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((side - 1u) & 3u) << 30u), ((side - 1u) >> 2u) | ((side - 1u) << 14u), 0xfacu | (TileR64KBX << 20u) | (Type2D << 28u), 0u, @@ -340,13 +340,13 @@ std::array Descriptor(std::uint64_t address, std::uint32_t sid }; } -std::shared_ptr Sampled(const Context& context, std::uint64_t address, std::uint32_t side = Side) { - const auto words = Descriptor(address, side); +std::shared_ptr Sampled(const Context& context, std::uint64_t address, std::uint32_t side = Side, std::uint32_t format = Format32UInt) { + const auto words = Descriptor(address, side, format); return CachedSampledTexture(context, words); } -GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side) { - const auto words = Descriptor(address, side); +GuestTextureResource Resource(std::uint64_t address, std::uint32_t side = Side, std::uint32_t format = Format32UInt) { + const auto words = Descriptor(address, side, format); return DecodeTextureResource(words); } @@ -1039,13 +1039,14 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL RecoveryProc(VkDevice device, const cha return recovery.deviceProc(device, name); } -void recoveryTests(Context context, bool storage) { +void recoveryTests(Context context, const std::string& mode) { recovery = RecoveryProbe{}; recovery.deviceProc = context.deviceProc; context.deviceProc = RecoveryProc; context.cacheReclaim = ReclaimForTest; context.bufferPool.reset(); Block block(context); + const bool storage = mode == "recover-storage"; if (storage) { auto oldest = CachedStorageSurface(context, Resource(block.Surface(0))); const auto allocated = oldest->AllocationBytes(); @@ -1061,7 +1062,7 @@ void recoveryTests(Context context, bool storage) { Require(recovery.released == allocated, "storage allocation recovery counted memory still held by its caller"); pending->Flush(); Require(!pending->Dirty(), "pending storage results could not be stored after memory recovery"); - } else { + } else if (mode == "recover-sampled") { auto oldest = Sampled(context, block.Surface(0)); Require(!oldest->ViewsStorageImage(), "the sampled recovery fixture needs a snapshot allocation"); const auto allocated = oldest->AllocationBytes(); @@ -1074,9 +1075,43 @@ void recoveryTests(Context context, bool storage) { Require(made != nullptr && released.expired(), "sampled allocation recovery did not release its own oldest unused texture"); Require(Sampled(context, block.Surface(1)) == held, "sampled allocation recovery removed a texture still held by its caller"); Require(recovery.released == allocated, "sampled allocation recovery counted memory still held by its caller"); + } else { + const bool srgb = mode == "recover-srgb"; + if (srgb) { + context.srgbDecodeFormats = 0; + context.srgbCopyFormats |= 1u; + } + auto oldest = Sampled(context, block.Surface(0)); + Require(!oldest->ViewsStorageImage(), "the sampled recovery fixture needs a snapshot allocation"); + const auto allocated = oldest->AllocationBytes(); + std::weak_ptr released = oldest; + oldest.reset(); + auto held = Sampled(context, block.Surface(1)); + const auto side = srgb ? 1024u : 512u; + const auto format = srgb ? 128u : 43u; + const auto sourceWords = Descriptor(block.Surface(4), side, format); + const auto sourceResource = DecodeTextureResource(sourceWords); + auto source = CachedStorageSurface(context, sourceResource); + source->MarkDirty(); + if (srgb) { + Require(Texture::CanSrgbExpandFrom(context, *source, sourceResource), "the storage surface cannot serve an sRGB expansion copy"); + } else { + Require(Texture::CanRepackCopyFrom(*source, sourceResource), "the storage surface cannot serve a repacked copy"); + } + recovery.pressure = true; + auto made = CachedSampledTexture(context, sourceWords); + recovery.pressure = false; + Require(made != nullptr && made->SharedStorageSource() == source, "sampled copy allocation recovery did not serve the pending storage image"); + Require(released.expired(), "sampled copy allocation recovery did not release its own oldest unused texture"); + Require(source->Dirty() && StorageImageCached(context, source.get()), "sampled copy allocation recovery changed or evicted an image with pending results"); + Require(Sampled(context, block.Surface(1)) == held, "sampled copy allocation recovery removed a texture still held by its caller"); + Require(recovery.released == allocated, "sampled copy allocation recovery counted memory still held by its caller"); + source->Flush(); + Require(!source->Dirty(), "pending storage results could not be stored after memory recovery"); } Require(recovery.requests == 1 && recovery.failures >= 1 && recovery.failures <= 2, "image allocation recovery was skipped or retried without a bound"); - std::printf("%s image allocation recovered after %u injected failures: %llu bytes released\n", storage ? "storage" : "sampled", recovery.failures, static_cast(recovery.released)); + const char* label = storage ? "storage" : mode == "recover-srgb" ? "sRGB" : mode == "recover-repack" ? "repacked" : "sampled"; + std::printf("%s image allocation recovered after %u injected failures: %llu bytes released\n", label, recovery.failures, static_cast(recovery.released)); } } @@ -1091,7 +1126,7 @@ int main(int argc, char** argv) { } const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; - const bool recover = mode == "recover-sampled" || mode == "recover-storage"; + const bool recover = mode == "recover-sampled" || mode == "recover-storage" || mode == "recover-srgb" || mode == "recover-repack"; #ifdef _WIN32 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", recover ? "16" : "1"); if (std::getenv("APS5_TEMPLATE_AUDIT_MS") == nullptr) _putenv_s("APS5_TEMPLATE_AUDIT_MS", "250"); @@ -1158,7 +1193,7 @@ int main(int argc, char** argv) { } if (recover) { std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); - recoveryTests(context, mode == "recover-storage"); + recoveryTests(context, mode); return 0; } { From 042a53079da2cc3106103d424731d667b2728c00 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:50:13 -0300 Subject: [PATCH 239/318] test(agc): select supported sRGB copy profile --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../libSceAgcDriver/tests/execution/SampledCacheBudget.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 52b265a546..30830985c7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2794,6 +2794,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endforeach() + set_property(TEST agc_driver_recover_srgb_allocation APPEND PROPERTY ENVIRONMENT_MODIFICATION "APS5_SRGB_SHADER_DECODE=set:copy") add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized) add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 2fe3e34065..bc73221190 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureCache.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" @@ -1078,8 +1079,9 @@ void recoveryTests(Context context, const std::string& mode) { } else { const bool srgb = mode == "recover-srgb"; if (srgb) { - context.srgbDecodeFormats = 0; - context.srgbCopyFormats |= 1u; + context.srgbDecodeFormats = SrgbDecodeFormats(context.formatProperties, context.physical); + context.srgbCopyFormats = SrgbCopyFormats(context.formatProperties, context.physical, context.srgbDecodeFormats); + Require(SrgbExpandsToRgba8(context, 128u), "the device cannot serve the sRGB expansion copy fixture"); } auto oldest = Sampled(context, block.Surface(0)); Require(!oldest->ViewsStorageImage(), "the sampled recovery fixture needs a snapshot allocation"); From 8c521d7d1e66eedac4e36c6121c700dc4a6bf2ed Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:22:15 -0300 Subject: [PATCH 240/318] fix(agc): recover initial sampled image copies --- .../libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp | 4 ++-- core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp | 4 ++-- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 09f1984c79..b7bcf3311a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -925,10 +925,10 @@ std::shared_ptr cachedTexture(const Context& context, std::spanRefreshDepthCopy(); } else if (IsRepackedTextureFormat(resource.format)) { - entry.texture = std::make_shared(context, source, resource, components, Texture::RepackCopyTag{}); + entry.texture = createTexture(source, resource, components, Texture::RepackCopyTag{}); entry.texture->RefreshDepthCopy(); } else if (srgbExpand) { - entry.texture = std::make_shared(context, source, resource, components, Texture::SrgbExpandTag{}); + entry.texture = createTexture(source, resource, components, Texture::SrgbExpandTag{}); entry.texture->RefreshDepthCopy(); } else { entry.texture = createTexture(source, resource, components); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 5060743d4a..85a8466970 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -282,7 +282,7 @@ ExpandImage CreateExpandImage(const Context& context, const GuestTextureResource VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); - result.owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy"); + result.owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory sRGB copy", true); Check(context.Function("vkBindImageMemory")(context.device, result.image, result.owned->memory, 0), "vkBindImageMemory sRGB copy"); result.bytes = requirements.size; return result; @@ -642,7 +642,7 @@ Texture::Texture(const Context& context, const std::shared_ptr& allocation.allocationSize = requirements.size; allocationBytes = requirements.size; allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy"); + owned->Allocate(context, allocation, requirements.memoryTypeBits, "vkAllocateMemory repacked copy", true); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory repacked copy"); const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index b573617dbf..20f6a75f9b 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -69,7 +69,7 @@ - [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. - [Workgroup count split](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) (libSceAgcDriver) - a direct dispatch over `maxComputeWorkGroupCount` runs as `vkCmdDispatch` chunks, each bound to a pipeline variant whose workgroup offset specialization constants add the chunk's base to `WorkgroupId` and `GlobalInvocationId`. A variant is built when its chunk base is first recorded, not when the dispatch is prepared. A split of a shader with device-memory LDS throws: its slot base reads the chunk's `NumWorkgroups`, not the dispatch's. Chunks are ordered by a compute barrier, which the console does not have between workgroups of one dispatch. -- [GPU allocation recovery](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - recovery may free too little and still throw: caller-held sampled images, pending storage results and recorder references stay alive. Budgets remain floors rather than hard device limits. Image and buffer allocation failures trim idle buffers, then sampled and clean storage cache entries. A held cache lock is reused only for a new image's allocation after lookup iterators are no longer used; hit refreshes and flushes skip that cache. Other cache, resource and device locks are tried without waiting. Fault-injection fixtures passed on Windows 11 (AMD RX 6700 XT, AMD Vulkan 26.8.1); the integrated image-allocation wrapper passed on Linux (AMD RX 9070 XT, RADV). Other hardware driver stacks are unverified. +- [GPU allocation recovery](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - recovery may free too little and still throw: caller-held sampled images, pending storage results and recorder references stay alive. Budgets remain floors rather than hard device limits. Image and buffer allocation failures trim idle buffers, then sampled and clean storage cache entries. A held cache lock is reused only for a new image's allocation after lookup iterators are no longer used; hit refreshes and flushes skip that cache. Other cache, resource and device locks are tried without waiting. - [Scan-resistant sampled texture admission](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - while the sampled texture cache is over its budget but within a quarter over it, the first use of a texture it does not hold in a present is refused (used for its draw, then released, never cached) when its least recently used entry was used in the current or previous present, and a later use in the same present is refused when that entry was used in the current present. So a working set larger than the budget keeps a stable subset: a texture used more than once in a present still evicts an entry not yet used in that present, so a working set with interleaved multi-use textures rotates as on main, and the textures that arrive after the cache fills are created again at each use for as long as the least recently used entry is from the current present, including in the transition present after a scene change. Past a quarter over the budget the entries are evicted as before, so after the budget falls the cache can sit at up to 125% of the reduced budget until entries stop being used. Entries unused for two presents are evicted as before. A later use can evict an entry from the previous present and then be refused when the next least recently used entry is from the current present, so that entry is created again in the next present. The "current or previous present" window is an unmeasured threshold: it was chosen, not derived. Measured once, as one A-B-A series on one title (loading phase about 33000 textures per frame against a 5.4 GiB budget) on one RTX 4090, 3600 s each: 932 / 968 / 941 frames, evictions after frame 620 2.72M -> 1.17M, texture creations unchanged (about 5.8M). Nothing is measured on other titles or GPUs, and a title whose per-present working set only slightly exceeds the budget now re-creates the excess textures at every use. - [Texture cache budgets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - the storage image cache's budget is what the reported heap budget leaves after the heap's use other than the cached storage images (at least the sampled textures alive) and an eighth of the budget, capped at a quarter of the budget and at `TextureCacheBudget` (a quarter of the heap's size, at least 2 GiB), never below 512 MiB; the sampled cache's budget reserves `TextureCacheBudget` for the storage cache and never falls below 2 GiB. The storage budget keeps no room for the part of that 2 GiB floor the sampled textures alive do not use: it gives room back as they grow, at its next read. As the heap's other use grows the sampled budget reaches its floor first, then the storage budget falls. Each cache trims only at its next insert, a storage image is allocated before its insert reads the budget, and both budgets read the heap at most once a second, so a burst between two reads, sampled textures included, can still exceed the reported budget. The eighth is the sampled formula's value reused and the 512 MiB floor and the caps the earlier ones; none was measured beyond PPSA11386 on one 10 GB GPU and PPSA16064 on one 8 GB GPU, and a title whose storage working set exceeds the shrunk budget re-creates storage images at each use. - [Polygon primitive type](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_PRIMITIVE_TYPE` 21 (`PrimitiveType::Polygon`) decodes to `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN` and is treated as inactive for vertex point size, like triangle fans and strips. PPSA01317 (Gran Turismo 7) submits polygon draws; whether the console performs polygon-mode decomposition or custom vertex routing is unverified. From 91b1fd1c6b3d78b491eb0f9dfbeb84a33c493f33 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 20:25:04 -0300 Subject: [PATCH 241/318] test(agc): check retention before cache refresh --- .../libSceAgcDriver/tests/execution/SampledCacheBudget.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index bc73221190..6c414d6cda 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -1106,7 +1106,9 @@ void recoveryTests(Context context, const std::string& mode) { Require(made != nullptr && made->SharedStorageSource() == source, "sampled copy allocation recovery did not serve the pending storage image"); Require(released.expired(), "sampled copy allocation recovery did not release its own oldest unused texture"); Require(source->Dirty() && StorageImageCached(context, source.get()), "sampled copy allocation recovery changed or evicted an image with pending results"); - Require(Sampled(context, block.Surface(1)) == held, "sampled copy allocation recovery removed a texture still held by its caller"); + Require(!held->Released() && held.use_count() == 2, "sampled copy allocation recovery removed a texture still held by its caller"); + const auto refreshed = Sampled(context, block.Surface(1)); + Require(refreshed != nullptr && (refreshed == held || refreshed->ViewsStorageImage()), "sampled copy cache refresh did not serve the surface"); Require(recovery.released == allocated, "sampled copy allocation recovery counted memory still held by its caller"); source->Flush(); Require(!source->Dirty(), "pending storage results could not be stored after memory recovery"); From 5836873170754a1cce584944b6dd1fb17d3ad3ad Mon Sep 17 00:00:00 2001 From: QRMOE <217609482+QRMOE@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:15:29 +0200 Subject: [PATCH 242/318] fix(memory): honor low guest mapping hints --- core/libs/prx/libc/src/GuestArena.cpp | 15 ++++- core/libs/prx/libkernel/CMakeLists.txt | 12 ++++ .../libkernel/DirectMemory/PosixMemory.cpp | 2 +- core/libs/tests/GuestLowAddressMemory.cpp | 66 +++++++++++++++++++ core/libs/tests/GuestMemory.cpp | 8 ++- docs/dev/debt/libc.md | 2 +- docs/dev/debt/libkernel.md | 2 +- 7 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 core/libs/tests/GuestLowAddressMemory.cpp diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 20cdbc1820..6a3f41cfc3 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -46,7 +46,12 @@ void noteAccess(const void* pointer, std::size_t bytes) { } constexpr std::uintptr_t GuestArenaOffset = GuestArena::GuestAddressOffset; -constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull + GuestArenaOffset; +constexpr std::uintptr_t DefaultAllocationStart = 0x0000000200000000ull + GuestArenaOffset; +#ifdef __APPLE__ +constexpr std::uintptr_t ArenaStart = DefaultAllocationStart; +#else +constexpr std::uintptr_t ArenaStart = GuestArenaOffset == 0 ? 0x10000ull : DefaultAllocationStart; +#endif constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull + GuestArenaOffset; constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull + GuestArenaOffset; constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull + GuestArenaOffset; @@ -86,7 +91,7 @@ public: RequireAllocatable(alignment); std::lock_guard lock(_lock); auto candidate = hint < _end ? FindAtOrAbove(hint, bytes, alignment) : 0; - if (candidate == 0 && hint > _base) candidate = FindAtOrAbove(_base, bytes, alignment); + if (candidate == 0 && hint > _base) candidate = FindAtOrAbove(hint < DefaultAllocationStart ? _base : DefaultAllocationStart, bytes, alignment); return Claim(candidate, bytes); } @@ -273,6 +278,10 @@ private: const int failed = ReserveNoReplace(cursor, end - cursor); if (failed == 0) continue; if (failed != EEXIST) { + if (cursor < DefaultAllocationStart && (failed == EACCES || failed == EPERM)) { + occupied.emplace_back(cursor, end); + continue; + } char message[128]; std::snprintf(message, sizeof(message), "guest arena: cannot reserve 0x%llx+0x%llx", static_cast(cursor), static_cast(end - cursor)); @@ -309,7 +318,7 @@ private: } std::uintptr_t FindAtOrAbove(std::uintptr_t hint, std::size_t bytes, std::size_t alignment) const { - std::uintptr_t candidate = alignUp(std::max(_base, hint), alignment); + std::uintptr_t candidate = alignUp(hint == 0 ? DefaultAllocationStart : std::max(_base, hint), alignment); auto it = _used.upper_bound(candidate); if (it != _used.begin() && std::prev(it)->second > candidate) --it; for (; it != _used.end(); ++it) { diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2f39251297..aa785ec361 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1621,6 +1621,18 @@ if(BUILD_TESTING) set_tests_properties(guest_memory guest_mmap_fixed_anonymous PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(NOT ANYPS5_ENABLE_ASAN AND (WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux")) + add_test_executable(guest_low_address_memory_tests ${LIBS_INCLUDE_DIR}/tests/GuestLowAddressMemory.cpp) + target_include_directories(guest_low_address_memory_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_low_address_memory_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_low_address_memory_tests) + add_test(NAME guest_low_address_memory COMMAND guest_low_address_memory_tests) + set_tests_properties(guest_low_address_memory PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_low_address_memory PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endif() add_test_executable(guest_mprotect_unmapped_tests ${LIBS_INCLUDE_DIR}/tests/GuestMprotectUnmapped.cpp) target_include_directories(guest_mprotect_unmapped_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_mprotect_unmapped_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp index c540f34c99..8f449c1b16 100644 --- a/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp @@ -171,7 +171,7 @@ void* APS5_VABI mmap_nid_postfix(void* address, std::size_t length, int protecti #endif const bool exclusive = fixedAnonymous && (mappingFlags & GuestMapExclusive) != 0; const int placement = fixedAnonymous ? GuestMapFixed | (exclusive ? GuestMapNoOverwrite : 0) : 0; - void* mapped = fixedAnonymous ? address : nullptr; + void* mapped = address; const auto alignment = !fixedAnonymous && (flags & GuestAlignedSuper) != 0 ? SuperpageSize : PS5_PAGE_SIZE; try { const auto result = DoMapAnon(&mapped, rounded, protection, placement, alignment, exclusive, fixedAnonymous); diff --git a/core/libs/tests/GuestLowAddressMemory.cpp b/core/libs/tests/GuestLowAddressMemory.cpp new file mode 100644 index 0000000000..8627e2bf2d --- /dev/null +++ b/core/libs/tests/GuestLowAddressMemory.cpp @@ -0,0 +1,66 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI mmap_nid_postfix(void*, std::size_t, int, int, int, std::int64_t) noexcept; +int APS5_VABI munmap_nid_postfix(void*, std::size_t) noexcept; +int APS5_VABI mprotect_nid_postfix(void*, std::size_t, int) noexcept; +int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "Low guest memory check failed at line %u\n", static_cast(location.line())); + std::abort(); + } +} + +int main() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t bufferBytes = 64u << 20; + constexpr std::size_t stackBytes = 128u << 20; + constexpr std::uintptr_t lowLimit = 0x80000000; + const auto failed = reinterpret_cast(~std::uintptr_t{0}); + auto* buffer = static_cast(mmap_nid_postfix(reinterpret_cast(0x20000000), bufferBytes, 3, 0x1002, -1, 0)); + auto* stack = static_cast(mmap_nid_postfix(reinterpret_cast(0x40000000), stackBytes, 3, 0x1002, -1, 0)); + std::fprintf(stderr, "Low guest memory: buffer=%p stack=%p\n", static_cast(buffer), static_cast(stack)); + Require(buffer != failed && stack != failed); + const auto bufferAddress = reinterpret_cast(buffer); + const auto stackAddress = reinterpret_cast(stack); + Require(bufferAddress >= 0x20000000 && bufferAddress <= lowLimit - bufferBytes); + Require(stackAddress >= 0x40000000 && stackAddress <= lowLimit - stackBytes); + Require(GuestArena::GuestArenaContains_nid_postfix(buffer, bufferBytes)); + Require(GuestArena::GuestArenaContains_nid_postfix(stack, stackBytes)); + buffer[0] = 0x31; + buffer[bufferBytes - 1] = 0x32; + stack[0] = 0x41; + stack[stackBytes - 1] = 0x42; + + void* collision = mmap_nid_postfix(buffer, page, 3, 0x1002, -1, 0); + Require(collision != failed && collision != buffer); + Require(buffer[0] == 0x31 && buffer[bufferBytes - 1] == 0x32); + Require(munmap_nid_postfix(collision, page) == 0); + Require(mmap_nid_postfix(buffer, page, 3, 0x5012, -1, 0) == failed && buffer[0] == 0x31); + Require(mmap_nid_postfix(buffer, page, 3, 0x1012, -1, 0) == buffer); + Require(buffer[0] == 0 && buffer[bufferBytes - 1] == 0x32); + + Require(mprotect_nid_postfix(buffer, page, 1) == 0); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(buffer, 0, &info, sizeof(info)) == 0 && info.protection == 1); + Require(mprotect_nid_postfix(buffer, page, 3) == 0); + buffer[0] = 0x51; + + void* ordinary = mmap_nid_postfix(nullptr, page, 3, 0x1002, -1, 0); + Require(ordinary != failed && reinterpret_cast(ordinary) >= 0x200000000ull); + Require(munmap_nid_postfix(ordinary, page) == 0); + Require(munmap_nid_postfix(buffer, bufferBytes) == 0); + Require(munmap_nid_postfix(stack, stackBytes) == 0); + Require(mmap_nid_postfix(buffer, page, 3, 0x1002, -1, 0) == buffer); + Require(buffer[0] == 0); + Require(munmap_nid_postfix(buffer, page) == 0); +} diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index d03a35de70..b7238e7a70 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -1441,7 +1441,13 @@ static void CheckFixedMappingsReachTheApplicationAreaEnd() { 0; #endif constexpr std::uintptr_t applicationAreaEnd = 0xFC00000000ull + expectedOffset; - Require(base == 0x200000000ull + expectedOffset && base + size == applicationAreaEnd); + constexpr std::uintptr_t expectedBase = +#ifdef __APPLE__ + 0x200000000ull + expectedOffset; +#else + expectedOffset == 0 ? 0x10000ull : 0x200000000ull + expectedOffset; +#endif + Require(base == expectedBase && base + size == applicationAreaEnd); std::int64_t phys = 0; Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); void* const requested = reinterpret_cast(applicationAreaEnd - page * 2); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2ec10f5024..730ef9b09b 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -18,7 +18,7 @@ - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. - [feenableexcept, fedisableexcept, __feenableexcept, __fedisableexcept, fegettrapenable and fesettrapenable](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libc) change only the x87 masks under Rosetta 2: MXCSR keeps its six exception masks set there, so no SSE trap is enabled, and the enable and disable results, which report the traps enabled on both units, report none - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting -- [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws +- [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws. The console's low-address layout is not measured. Linux and Windows reserve free addresses from 64 KiB, except host mappings and low chunks the Linux address policy refuses; null hints still allocate from 8 GiB. macOS and ASan builds still start the arena at 8 GiB and cannot supply low 32-bit addresses. Guest thread creation ignores a stack address set in its attributes. A host-loaded executable is not moved, so a PIE above the arena gets no memory within 2 GiB of its code ([#5217](https://github.com/boykopovar/AnyPS5/issues/5217)) - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error - [sceLibcPafMspaceCreate, Destroy, Malloc, Memalign, PosixMemalign, Free, Calloc, Realloc, Reallocalign, MallocStats and MallocStatsFast](../../../core/libs/prx/libc/src/Mspace.cpp) - PAF exports delegate to `sceLibcMspace` without malloc hooks; `Memalign` and `PosixMemalign` forward to their `Mspace` counterparts with owner ignored and are unverified on console. `Create`'s options block (version, type 1 or 2 at +2, field at +0x10) throws when non-null as its meaning is unknown. Block footers are not modelled, so owners are ignored: on PS5 firmware 13.60 mismatched owners in `Reallocalign` or in-place `Realloc` kill the process and `MallocStats` returns 1 if non-heap-owned blocks exist, which is not reproduced. `MallocUsableSize`, `Trim`, `IsHeapEmpty`, `CheckMemoryBounds`, `ReportMemoryBlocks` and `GetFooterValue` are not implemented. diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 515c8d8c2b..f909081b56 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -75,7 +75,7 @@ - [sceKernelMapFlexibleMemory / sceKernelMapNamedFlexibleMemoryInternal](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 is accepted and ignored; its meaning is not established. KytyPS5 (`GUEST_MAP_UNKNOWN_8000`) and sharpemu accept it without reading it. Every known caller is an SDK 9 `libc.prx` heap, which passes it with read/write protection and uses the mapping as an older `libc.prx` uses `sceKernelMapNamedFlexibleMemory` with flags 0: through the Internal variant in PPSA21564, through `sceKernelMapFlexibleMemory` in PPSA22520 01.000.011 - [sceKernelMtypeprotect](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - sets the memory type of the direct mappings in the page-rounded range and of the direct memory behind them, as shadPS4 and prosper do (KytyPS5 ignores the type); the type is not validated, flexible pages in the range only get the protection, and a later map of the retyped direct memory reports the type it was allocated with. `sceKernelBatchMap2` operation 4 takes the same path with the entry type - [Map hints inside a reservation](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMapDirectMemory` and the flexible and anonymous maps place a mapping without `MAP_FIXED` at its hint address when the whole range lies in a range still reserved by `sceKernelReserveVirtualRange`, as if `MAP_FIXED` were set. Taken from PPSA01474, which reserves 64 GiB and then maps its heaps inside it with hints and no `MAP_FIXED`, and uses the hint addresses afterwards; not measured on the console. Hints outside a reservation, or over a range that is partly mapped, stay hints -- [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB and fixed addresses must be aligned. The complete rounded range must lie inside the guest arena or, for POSIX fixed anonymous `mmap` on Windows, one current or registered native thread stack; other mapping APIs do not opt into stack replacement. Native stack replacement keeps the reservation, decommits and recommits fresh pages, and rejects foreign allocations, ranges spanning reservations and host `PAGE_GUARD` pages before mutation. Current-stack ranges must end at least 64 KiB below the active host frame; callers replacing another thread's stack must coordinate with that thread. If recommitting fails, the call returns `ENOMEM` with the range still reserved; thread cleanup attempts to recommit it before restoring write access. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. AnyPS5 starts the guest arena at 0x200000000, so the fixed path rejects the low 0x400000 address requested by PICO-8 PPSA99808. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. +- [Fixed anonymous mmap](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) - `MAP_PRIVATE | MAP_ANON | MAP_FIXED` replaces the requested range through the existing guest mapping machinery and zeroes the new memory. `MAP_EXCL` is checked under the allocation-registry mutation lock, including virtual reservations, and returns `ENOMEM` without changing an occupied range; without `MAP_FIXED` it is `EINVAL`. Lengths are rounded to 16 KiB and fixed addresses must be aligned. The complete rounded range must lie inside the guest arena or, for POSIX fixed anonymous `mmap` on Windows, one current or registered native thread stack; other mapping APIs do not opt into stack replacement. Native stack replacement keeps the reservation, decommits and recommits fresh pages, and rejects foreign allocations, ranges spanning reservations and host `PAGE_GUARD` pages before mutation. Current-stack ranges must end at least 64 KiB below the active host frame; callers replacing another thread's stack must coordinate with that thread. If recommitting fails, the call returns `ENOMEM` with the range still reserved; thread cleanup attempts to recommit it before restoring write access. This follows FreeBSD 11.4 [argument validation and rounding](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L226-L299), [fixed replacement](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1455-L1464), and [collision errno](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1604-L1613). The console behaviour has not been measured. Linux and Windows accept addresses from 64 KiB that no host mapping occupies, such as the 0x400000 imported by PICO-8 PPSA99808; macOS keeps the 0x200000000 start. Non-fixed private anonymous mappings keep their hint; null hints still use the 8 GiB area. On Windows the host's `KUSER_SHARED_DATA` always occupies 0x7ffe0000, so Wine's shared user data page (Forever Launcher PPSA99030) cannot be placed there. The console address-space layout is unmeasured; executable placement outside the host arena is tracked in #5217. Static catalog disassembly finds `0x1012` in PPSA98001/PPSA99808 and `0x5012` in PPSA99808; these titles were not run. - [Mapping hints with no room above them](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - a mapping without `MAP_FIXED` whose hint lies above the guest address space, or has no free range at or above it, is placed from the bottom of the guest arena, as FreeBSD 11.4's [`vm_mmap_object`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_mmap.c#L1569) retries through [`vm_map_find_min`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/vm/vm_map.c#L1555-L1584) from its minimum address. FreeBSD's minimum is the end of the largest possible data segment rather than the start of the arena, and the console's address-space layout is not measured. PS5CEMU-HAR (PPSA99360) gives `sceKernelReserveVirtualRange` a hint near its own code, which is a host address here because the relinked executable lies outside the guest arena, so its JIT gets memory more than 2 GiB from its code and does not start - [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [tcgetattr, tcsetattr](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) - every file and pipe descriptor is taken as not a terminal (`ENOTTY`), and a socket gets `EADDRNOTAVAIL` for `AF_INET` and `EOPNOTSUPP` otherwise, as FreeBSD's `in_control` and `pru_control_notsupp` answer a termios ioctl; what descriptors 0 to 2 of a title are on the console, and whether any of them is a terminal there, is unknown From 16d2cbb4b38534805b7e4926f210b86ff00b599a Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Fri, 23 Oct 2026 17:05:30 +0200 Subject: [PATCH 243/318] fix(libSceRtc): range-check by the compiled SDK version as the PS4 does Measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001. From 0x03000000 on, FormatRFC3339 returns INVALID_VALUE when the zone shift overflows or underflows the tick, GetDayOfWeek checks the month before the year and rejects years past 9999, and SetTime_t rejects negative seconds and converts all 64 bits. 0x02ffffff and earlier keep the wrap, the year-first order and the low 32 bits. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceRtc/CMakeLists.txt | 1 + core/libs/prx/libSceRtc/Export.cpp | 27 ++++++++++++++-- core/libs/tests/GuestRtc.cpp | 44 ++++++++++++++++++++++++++ docs/dev/debt/libSceRtc.md | 1 + 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceRtc/CMakeLists.txt b/core/libs/prx/libSceRtc/CMakeLists.txt index e0b93b163e..689ae6f279 100644 --- a/core/libs/prx/libSceRtc/CMakeLists.txt +++ b/core/libs/prx/libSceRtc/CMakeLists.txt @@ -1,4 +1,5 @@ add_library(libSceRtc SHARED EXCLUDE_FROM_ALL Export.cpp) +target_link_libraries(libSceRtc PRIVATE libc) target_include_directories(libSceRtc PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceRtc) diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index a1dffea9ac..4b85e9449b 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -7,6 +7,7 @@ #include #include #include "SceTypes.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" namespace { @@ -31,6 +32,7 @@ constexpr std::int64_t WIN32_EPOCH_DAYS = 584388; constexpr std::uint64_t UNIX_EPOCH_TICK = UNIX_EPOCH_DAYS * TICKS_PER_DAY; constexpr std::uint64_t WIN32_EPOCH_TICK = WIN32_EPOCH_DAYS * TICKS_PER_DAY; constexpr std::uint64_t MAX_TICK = 3652059ull * TICKS_PER_DAY - 1; +constexpr std::uint32_t RANGE_CHECKED_SDK_VERSION = 0x03000000; struct ClockState { std::uint64_t tick = 0; @@ -100,6 +102,13 @@ RtcDateTime fromTick(std::uint64_t tick) { static_cast(timeOfDay % TICKS_PER_MINUTE / TICKS_PER_SECOND), static_cast(timeOfDay % TICKS_PER_SECOND)}; } +bool rangeChecked() { + const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); + std::uint32_t version; + std::memcpy(&version, process + 16, sizeof(version)); + return version >= RANGE_CHECKED_SDK_VERSION; +} + std::uint64_t currentTick(std::size_t clock = 0) { ClockState state; { @@ -370,8 +379,13 @@ int APS5_VABI sceRtcGetDaysInMonth(int year, int month) { } int APS5_VABI sceRtcGetDayOfWeek(int year, int month, int day) { - if (year < 1) return SCE_RTC_ERROR_INVALID_YEAR; - if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + if (rangeChecked()) { + if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + if (year < 1 || year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; + } else { + if (year < 1) return SCE_RTC_ERROR_INVALID_YEAR; + if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + } if (day < 1 || day > daysInMonth(year, month)) return SCE_RTC_ERROR_INVALID_DAY; return static_cast((daysFromCivil(year, static_cast(month), static_cast(day)) + 1) % 7); } @@ -451,6 +465,11 @@ int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds) { int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds) { if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (rangeChecked()) { + if (seconds < 0) return SCE_RTC_ERROR_INVALID_VALUE; + *time = fromTick(UNIX_EPOCH_TICK + static_cast(seconds) * TICKS_PER_SECOND); + return 0; + } *time = fromTick(UNIX_EPOCH_TICK + static_cast(static_cast(seconds)) * TICKS_PER_SECOND); return 0; } @@ -590,7 +609,9 @@ int APS5_VABI sceRtcFormatRFC3339(char* dateTime, const RtcTick* utc, int timeZo utc = &now; } if (timeZoneMinutes < -1439 || timeZoneMinutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; - const RtcTick local{utc->tick + static_cast(static_cast(timeZoneMinutes) * TICKS_PER_MINUTE)}; + const auto shift = static_cast(timeZoneMinutes) * TICKS_PER_MINUTE; + if (rangeChecked() && (shift < 0 ? utc->tick < static_cast(-shift) : utc->tick > std::numeric_limits::max() - static_cast(shift))) return SCE_RTC_ERROR_INVALID_VALUE; + const RtcTick local{utc->tick + static_cast(shift)}; if (local.tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_YEAR; const RtcDateTime time = fromTick(local.tick); int written = std::snprintf(dateTime, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 1746ac5f31..1d72b948e4 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -1,5 +1,8 @@ #include "SceTypes.hpp" +#include "ProcessParametersFixture.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include @@ -113,7 +116,14 @@ static int CallAdd(Add function, RtcTick* destination, const RtcTick* source, st std::abort(); } +static void SetCompiledSdkVersion(std::byte* process, std::uint32_t version) { + std::memcpy(process + 16, &version, sizeof(version)); +} + int main() { + static std::array process{}; + Require(ProcessParametersFixture::Install(process.data(), process.size())); + SetCompiledSdkVersion(process.data(), 0x01000051); constexpr int invalidPointer = static_cast(0x80B50002); constexpr int invalidValue = static_cast(0x80B50003); constexpr int badParse = static_cast(0x80B50007); @@ -952,6 +962,40 @@ int main() { Require(sceRtcGetCurrentDebugNetworkTick(¤t) == 0 && current.tick >= debugClock.tick && current.tick < debugClock.tick + 1000000); Require(sceRtcGetCurrentRawNetworkTick(¤t) == 0 && current.tick > unixEpochTick); } + SetCompiledSdkVersion(process.data(), 0x02ffffff); + Require(sceRtcGetDayOfWeek(0, 13, 1) == invalidYear && sceRtcGetDayOfWeek(10000, 1, 1) == 6); + SetCompiledSdkVersion(process.data(), 0x03000000); + Require(sceRtcGetDayOfWeek(0, 13, 1) == invalidMonth && sceRtcGetDayOfWeek(10000, 1, 1) == invalidYear); + SetCompiledSdkVersion(process.data(), 0x05050001); + Require(sceRtcGetDayOfWeek(0, 13, 1) == invalidMonth && sceRtcGetDayOfWeek(10000, 13, 1) == invalidMonth && sceRtcGetDayOfWeek(2024, 0, 1) == invalidMonth); + Require(sceRtcGetDayOfWeek(0, 1, 0) == invalidYear && sceRtcGetDayOfWeek(10000, 2, 30) == invalidYear && sceRtcGetDayOfWeek(65536, 1, 1) == invalidYear); + Require(sceRtcGetDayOfWeek(2024, 2, 30) == invalidDay && sceRtcGetDayOfWeek(9999, 12, 31) == 5 && sceRtcGetDayOfWeek(1, 1, 1) == 1); + Require(sceRtcGetDaysInMonth(0, 13) == invalidYear && sceRtcGetDaysInMonth(10000, 1) == 31 && sceRtcIsLeapYear(10000) == 1); + RtcDateTime sentinel{0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaaaaaau}; + RtcDateTime ranged = sentinel; + Require(sceRtcSetTime_t(&ranged, -1) == invalidValue && Equal(ranged, sentinel)); + Require(sceRtcSetTime_t(&ranged, std::numeric_limits::min()) == invalidValue && Equal(ranged, sentinel)); + Require(sceRtcSetTime_t(&ranged, 4294967296ll) == 0 && Equal(ranged, RtcDateTime{2106, 2, 7, 6, 28, 16, 0})); + Require(sceRtcSetTime_t(&ranged, 253402300800ll) == 0 && Equal(ranged, RtcDateTime{10000, 1, 1, 0, 0, 0, 0})); + Require(sceRtcSetTime_t(&ranged, 18446744073709ll) == 0 && Equal(ranged, RtcDateTime{1969, 12, 31, 23, 59, 59, 448384})); + Require(sceRtcSetTime_t(&ranged, std::numeric_limits::max()) == 0 && Equal(ranged, RtcDateTime{1969, 12, 31, 23, 59, 59, 0})); + char ranges[64]; + RtcTick shifted{0}; + Require(sceRtcFormatRFC3339(ranges, &shifted, -1) == invalidValue); + shifted.tick = 59999999; + Require(sceRtcFormatRFC3339(ranges, &shifted, -1) == invalidValue); + shifted.tick = 60000000; + Require(sceRtcFormatRFC3339(ranges, &shifted, -1) == 0 && std::strcmp(ranges, "0001-01-01T00:00:00.00-00:01") == 0); + shifted.tick = std::numeric_limits::max(); + Require(sceRtcFormatRFC3339(ranges, &shifted, 1) == invalidValue && sceRtcFormatRFC3339(ranges, &shifted, 1439) == invalidValue); + shifted.tick = std::numeric_limits::max() - 60000000 + 1; + Require(sceRtcFormatRFC3339(ranges, &shifted, 1) == invalidValue); + shifted.tick = std::numeric_limits::max() - 60000000; + Require(sceRtcFormatRFC3339(ranges, &shifted, 1) == invalidYear); + shifted.tick = 1ull << 63; + Require(sceRtcFormatRFC3339(ranges, &shifted, 0) == invalidYear && sceRtcFormatRFC3339(ranges, &shifted, -1) == invalidYear); + shifted.tick = maxTick + 1; + Require(sceRtcFormatRFC3339(ranges, &shifted, -1) == 0 && std::strcmp(ranges, "9999-12-31T23:59:00.00-00:01") == 0); Require(sceRtcInit() == 0); Require(sceRtcEnd() == 0); } diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index affc0d5d23..dbdbc7f26d 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -8,6 +8,7 @@ - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_YEAR` when the zone-shifted tick is past 9999 as a signed value, otherwise `INVALID_VALUE` when the source tick or the zone-shifted tick is negative as a signed value, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick formatting the current time and the ±1439 minute offset limit come from the PS5 measurement in the next entry and are not measured on the PS4 +- [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console From 54158953772526ea3dbdd21d53d93d461d153f38 Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 12:28:46 -0300 Subject: [PATCH 244/318] docs(libc): record reallocf as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 561aced31f..efc1981c97 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -24,7 +24,7 @@ - [malloc_usable_size](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - with the default allocator it returns the size the block was requested with (the range registered for it), not the rounded-up chunk size Sony's allocator reports; both satisfy "at least the requested size", and bytes past the request would not be write-watched. With an application allocator it calls entry 9 of the replacement table (`malloc_usable_size` in KytyPS5's `MallocReplace` order) and throws when the table leaves it null, like `malloc_stats_fast` - [malloc_stats_fast](../../../core/libs/prx/libc/src/GuestHeap.cpp) (libc) - with the default allocator it fills the 0x28-byte managed-size block as `sceLibcMspaceMallocStatsFast` does, from the default heap's own accounting, not Sony's allocator: the system sizes count the 1 MiB spans of its small-block classes and its committed large blocks, cached free ones included, and the in-use sizes count each handed-out block at its size-class or 64 KiB-granule size, header included. A block size below 0x28 or a null pointer returns 22, as in the mspace version; the version field is not checked. Ship of Harkinian (PPSA99320) calls it about 10 s after start-up. - [malloc, calloc, realloc, memalign, aligned_alloc, reallocalign and posix_memalign](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) - the console's own heap was measured in a payload process on firmware 13.60, not in a title. A heap a title replaces was not measured and keeps the FreeBSD jemalloc behaviour: `ENOMEM` on failure, `EINVAL` for an alignment that is zero or not a power of two, and `realloc(ptr, 0)` frees the block. With the console's heap, failures set `EINVAL`, and `errno` is left alone for a size above `SIZE_MAX - 0x1008f` (`SIZE_MAX - (A + 0x100af)` for an alignment over 32 rounded up to A), a `calloc` overflow or an alignment of 2^63 or more. Alignments round up to a power of two, and `realloc(ptr, 0)` keeps the block with 0 usable bytes. `reallocalign(ptr, 0, alignment)` for a block without that alignment was not measured and throws -- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, not measured on the console: size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails, the block is freed through the same allocator and null is returned with the `errno` that `realloc` left. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` +- [reallocf](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libSceLibcInternal, implemented in libc) - follows FreeBSD 11 `lib/libc/stdlib/reallocf.c` on top of this libc's `realloc`, as a payload process on a PS5 (firmware 13.60) does with the default heap (null and 0, size 0, 2^40 with `errno` 22 and the block freed, `SIZE_MAX` with `errno` untouched): size 0 does what `realloc` does here, keeping the block with 0 usable bytes with the default heap and freeing it and returning null with an application allocator, so the block is not freed twice; when `realloc` fails, the block is freed through the same allocator and null is returned with the `errno` that `realloc` left. Other failures (a commit failure that is not out of memory, an unknown pointer) throw, as they do for `realloc` - [sceLibcMspaceReallocalign](../../../core/libs/prx/libc/src/Mspace.cpp) (libSceLibcInternal, implemented in libc) takes `(void* heap, void* block, size_t alignment, size_t size)`, the order of the console's `libSceLibcInternal.sprx` (SDK 4.5x build of `J03950001`, 0x13880, whose internal routine at 0x12390 treats the third argument as the alignment and, for a null block, passes it to the routine `sceLibcMspaceMemalign` uses). Measured on a PS5 (firmware 13.60) from a homebrew payload: a null block with alignment 4096 and size 100 returns a 4096-aligned block; an alignment of 0 or 3 succeeds and 100 gives a 128-aligned block, as the routine raises it to at least 32 and to the next power of two; an alignment of 2^63 or more returns null, keeps the block and leaves `errno` alone; and a size of 0 returns a block (the same one when it is already suitable) instead of freeing it. A block that is aligned and large enough is kept where it is, without giving back the bytes past the new size as the console's in-place resize does - [acosl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, like the other libc math functions here, not by the console's (FreeBSD msun ld80 code in Sony's `libc.prx`): results can differ from the console's in the last bit, and on Linux glibc sets `errno` to `EDOM` for `acosl` outside [-1, 1] where msun only raises the floating-point exception. `long double` is the x87 80-bit format, as Sony's `frexpl` uses it (`fldt 0x8(%rsp)`, result in `st(0)`) - [atanl, sinl, cosl, tanl, sinhl, coshl, tanhl, expl, exp2l, logl, log10l and powl](../../../core/libs/prx/libc/src/Math.cpp) (libc) - computed by the host's math library, as `acosl` is, not by the console's FreeBSD msun ld80 code: results can differ from the console's in the last bit (glibc's `log10l(1000)` is not exactly 3), and on Linux glibc sets `errno` to `EDOM` or `ERANGE` where msun only raises the floating-point exception. `ldexpl` and `truncl` are exact operations and give the console's result; `logb` and `remainder` give the console's value too, but on Linux glibc sets `errno` (`ERANGE` for `logb(0)`, `EDOM` for `remainder` by zero or of an infinity) where msun only raises the floating-point exception From 0ae3cbcafb2131e07e6e592cc64ef7083eb322b7 Mon Sep 17 00:00:00 2001 From: StoneDC <19905433+StoneDC@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:39:10 +0800 Subject: [PATCH 245/318] fix(libScePad): retain explicit throw for unsupported Lotus IR and virtual-device exports --- core/libs/prx/libScePad/Export.cpp | 25 +++++++++++---------- core/libs/tests/GuestPad.cpp | 35 ++++++++++++++++++++---------- docs/dev/debt/libScePad.md | 3 --- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index dd6c2394f3..27321c0efd 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -22,10 +22,7 @@ struct PadUsbDetectionState; struct PadVrUsbConnectionStatus; struct PadDeviceInfo; struct PadLicenseControllerInformation; -struct PadRemoteSetting { - std::uint8_t data[64]; -}; -static_assert(sizeof(PadRemoteSetting) == 64); +struct PadRemoteSetting; struct PadVirtualTriggerSetting; struct PadProcessFocusParameter; struct PadDataExtended; @@ -666,13 +663,15 @@ int APS5_VABI scePadGetLotusIrCodeSetResult(std::int32_t handle, std::int32_t* r } int APS5_VABI scePadSetLotusEnableIrKey(std::int32_t handle) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - return PAD_OK; + (void)handle; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t handle) { - if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; - return PAD_OK; + (void)handle; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI scePadGetUsbDetectionState(std::int32_t deviceId, PadUsbDetectionState* state) { @@ -1021,8 +1020,10 @@ int APS5_VABI scePadVirtualDeviceInsertTriggerInternalState(std::int32_t deviceI } int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t deviceId, PadRemoteSetting* setting) { - if (setting == nullptr || deviceId <= 0) return PAD_ERROR_INVALID_ARG; - return PAD_ERROR_DEVICE_NOT_CONNECTED; + (void)deviceId; + (void)setting; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(std::int32_t handle, const PadVirtualTriggerSetting* setting) { @@ -1031,7 +1032,9 @@ int APS5_VABI scePadVirtualDeviceSetTriggerEffectSetting(std::int32_t handle, co } int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(bool disabled) { - return PAD_OK; + (void)disabled; + NotImplemented_nid_no_patch(__func__); + return 0; } int APS5_VABI scePadSetShareButtonMaskForRemotePlay(std::uint8_t mask) { diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 2783ffce72..cd4bf6a6d7 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,16 @@ int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t, void*); static void Require(bool value) { if (!value) std::abort(); } +template +static bool Throws(TCall call, const char* function) { + try { + static_cast(call()); + } catch (const std::runtime_error& error) { + return std::strstr(error.what(), function) != nullptr; + } + return false; +} + static void CheckInputCommands() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { @@ -497,6 +508,8 @@ int main() { Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); Require(scePadGetHandle(user, 0, 1) == noHandle); + Require(Throws([&] { return scePadSetLotusEnableIrKey(handle); }, "scePadSetLotusEnableIrKey")); + Require(Throws([&] { return scePadSetLotusDisableIrKey(handle); }, "scePadSetLotusDisableIrKey")); Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); @@ -506,15 +519,15 @@ int main() { Require(scePadSetAngularVelocityDeadbandState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetAngularVelocityBiasCorrectionState(handle, false) == PAD_OK); Require(scePadSetAngularVelocityBiasCorrectionState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); - Require(scePadSetLotusEnableIrKey(handle) == PAD_OK); - Require(scePadSetLotusEnableIrKey(handle + 1) == PAD_ERROR_INVALID_HANDLE); - Require(scePadSetLotusDisableIrKey(handle) == PAD_OK); - Require(scePadSetLotusDisableIrKey(handle + 1) == PAD_ERROR_INVALID_HANDLE); - Require(scePadVirtualDeviceDisableButtonRemapping(true) == PAD_OK); - Require(scePadVirtualDeviceDisableButtonRemapping(false) == PAD_OK); - std::array remoteSetting{}; - Require(scePadVirtualDeviceGetRemoteSetting(1, remoteSetting.data()) == static_cast(0x80920007)); - Require(scePadVirtualDeviceGetRemoteSetting(1, nullptr) == PAD_ERROR_INVALID_ARG); - Require(scePadVirtualDeviceGetRemoteSetting(0, remoteSetting.data()) == PAD_ERROR_INVALID_ARG); - Require(scePadVirtualDeviceGetRemoteSetting(-1, remoteSetting.data()) == PAD_ERROR_INVALID_ARG); + Require(Throws([&] { return scePadSetLotusEnableIrKey(handle + 1); }, "scePadSetLotusEnableIrKey")); + Require(Throws([&] { return scePadSetLotusDisableIrKey(handle + 1); }, "scePadSetLotusDisableIrKey")); + Require(Throws([] { return scePadVirtualDeviceDisableButtonRemapping(true); }, "scePadVirtualDeviceDisableButtonRemapping")); + Require(Throws([] { return scePadVirtualDeviceDisableButtonRemapping(false); }, "scePadVirtualDeviceDisableButtonRemapping")); + std::array scratch{}; + scratch.fill(0xAB); + Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(1, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); + Require(Throws([] { return scePadVirtualDeviceGetRemoteSetting(1, nullptr); }, "scePadVirtualDeviceGetRemoteSetting")); + Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(0, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); + Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(-1, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); + for (std::uint8_t byte : scratch) Require(byte == 0xAB); } diff --git a/docs/dev/debt/libScePad.md b/docs/dev/debt/libScePad.md index e740790bfe..c8fa44cf9d 100644 --- a/docs/dev/debt/libScePad.md +++ b/docs/dev/debt/libScePad.md @@ -3,8 +3,6 @@ ### Silent stubs - [scePadEnableExtensionPort](../../../core/libs/prx/libScePad/Export.cpp) returns success without changing extension-port state. The boolean argument is recovered; connected-device behavior is not modeled. -- [scePadSetLotusEnableIrKey and scePadSetLotusDisableIrKey](../../../core/libs/prx/libScePad/Export.cpp) return success on valid open handle without hardware Lotus IR key dispatching. -- [scePadVirtualDeviceDisableButtonRemapping](../../../core/libs/prx/libScePad/Export.cpp) returns success without changing virtual-device remapping tables. ### Unknown function info @@ -22,7 +20,6 @@ - [scePadIsDS4Connected, scePadIsBlasterConnected and scePadIsMoveConnected](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the `bool (int handle)` signatures are recovered; the library reports one DualSense to every title (see Touchpad resolution below), so no DualShock 4, Blaster or Move controller is ever behind a handle and each call answers false for any handle. The console's answer for an invalid handle is not measured - [scePadSetAngularVelocityBiasCorrectionState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - inferred signature `(int handle, bool enabled)` follows `scePadSetTiltCorrectionState` and `scePadSetAngularVelocityDeadbandState`; an invalid handle returns `PAD_ERROR_INVALID_HANDLE`, disabling is a no-op, and enabling throws because bias correction is not modelled - [scePadGetAccessibilityControllerState](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - the value a console reports for a DualSense is not measured; the emulated pad, which is not an Access controller, reports 0. The name hashes to NID `zBsgF0q8DIM`. In the OS 10.01 system module the `(int handle, uint32_t* state)` function returns `0x80920005` before `scePadInit`, then `INVALID_ARG` for a NULL `state` and `INVALID_HANDLE` for a bad handle, and otherwise asks the pad driver for a per-device value of the device behind the handle: 0 when no device is behind it, 1 when the driver has no answer, else the driver's value. What the values mean is unknown -- [scePadVirtualDeviceGetRemoteSetting](../../../core/libs/prx/libScePad/Export.cpp) (libScePad) - checks the output pointer and non-zero device id, returning `PAD_ERROR_DEVICE_NOT_CONNECTED` (0x80920007) when no virtual device is connected. Remote setting payload uses a 64-byte record whose internal field meanings remain unverified on hardware. - [libScePad internal data and device control](../../../core/libs/prx/libScePad/Export.cpp) - kernel-owned version, telemetry, Lotus-button and update payload fields remain unresolved. Lotus-button parameter units and the three blinking byte meanings are unknown. Port information occupies 152 bytes; history records occupy 72 bytes; Leonardo unique data occupies 24 bytes. Their complete field semantics remain unresolved. From 81820edf308cf54985a9bb5636ee327436cb19d2 Mon Sep 17 00:00:00 2001 From: emirplus Date: Fri, 23 Oct 2026 18:48:42 +0300 Subject: [PATCH 246/318] fix(tools): reconfigure streams with backslashreplace in unwrap_self Reconfigure sys.stdout and sys.stderr with backslashreplace error handling at the entrypoint of main() so unwrapping reports and error messages do not fail on Windows consoles configured with non-UTF-8 code pages. --- tools/tests/test_unwrap_self.py | 28 ++++++++++++++++++++++++++++ tools/unwrap_self.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index f30eb5a74f..964d6bc4af 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -779,5 +779,33 @@ class UnwrapTest(unittest.TestCase): self.assertTrue(output.exists()) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with mock.patch.object(unwrap_self.sys, "stdout", out), mock.patch.object(unwrap_self.sys, "stderr", err): + try: + unwrap_self.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 659f4eb07a..67261bc79d 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -336,7 +336,7 @@ def unwrap(path, output, verbose=False): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="unwrap a decrypted SELF or fSELF container into a clean ELF") parser.add_argument("input", help="SELF container to read") parser.add_argument("output", help="ELF executable to write") From dd45b07ce119ddc7d0862ceb9011e6ef33845048 Mon Sep 17 00:00:00 2001 From: emirplus Date: Fri, 23 Oct 2026 18:57:48 +0300 Subject: [PATCH 247/318] fix(tools): reconfigure streams with backslashreplace in select_tests Reconfigure sys.stdout and sys.stderr with backslashreplace error handling at the entrypoint of main() so test graph dumping and change diagnostics do not fail on Windows consoles configured with non-UTF-8 code pages. --- tools/select_tests.py | 2 +- tools/tests/test_select_tests.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tools/select_tests.py b/tools/select_tests.py index 80005e556d..6d8733a62c 100644 --- a/tools/select_tests.py +++ b/tools/select_tests.py @@ -263,7 +263,7 @@ def escape(name): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Select the tests whose build inputs a change touches") sub = parser.add_subparsers(dest="command", required=True) d = sub.add_parser("dump", help="write the test graph of a built tree") diff --git a/tools/tests/test_select_tests.py b/tools/tests/test_select_tests.py index b89ba0904a..07283fe840 100644 --- a/tools/tests/test_select_tests.py +++ b/tools/tests/test_select_tests.py @@ -392,5 +392,33 @@ class MissingCommandTests(unittest.TestCase): "ambiguous": None, "existing": [helper], "stale": None}) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with mock.patch.object(select_tests.sys, "stdout", out), mock.patch.object(select_tests.sys, "stderr", err): + try: + select_tests.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + + if __name__ == "__main__": unittest.main() From 21200356110d43762f4e3cb563a9cedc4e7d6f9f Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Fri, 23 Oct 2026 18:01:16 +0200 Subject: [PATCH 248/318] fix(libSceRtc): keep the pre-3.00 range rules to PS4 titles The compiled SDK version of a PS5 title counts from its own SDK, so an early PS5 title would have taken the PS4 pre-3.00 rules. Apply them only to a CUSA title, as libSceConvertKeycode tells PS4 titles apart; anything else, including a title without metadata, gets the later rules main had. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceRtc/CMakeLists.txt | 5 ++-- core/libs/prx/libSceRtc/Export.cpp | 16 +++++++++++++ core/libs/tests/GuestRtc.cpp | 33 +++++++++++++++++++++++++- docs/dev/debt/libSceRtc.md | 2 +- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceRtc/CMakeLists.txt b/core/libs/prx/libSceRtc/CMakeLists.txt index 689ae6f279..1411d4f936 100644 --- a/core/libs/prx/libSceRtc/CMakeLists.txt +++ b/core/libs/prx/libSceRtc/CMakeLists.txt @@ -1,5 +1,5 @@ add_library(libSceRtc SHARED EXCLUDE_FROM_ALL Export.cpp) -target_link_libraries(libSceRtc PRIVATE libc) +target_link_libraries(libSceRtc PRIVATE libkernel libc) target_include_directories(libSceRtc PRIVATE ${LIBS_INCLUDE_DIR}) configure_windows_unwind(libSceRtc) @@ -9,8 +9,9 @@ if(BUILD_TESTING) target_link_libraries(guest_rtc_tests PRIVATE libSceRtc libc) configure_windows_unwind(guest_rtc_tests) add_test(NAME guest_rtc COMMAND guest_rtc_tests) + add_test(NAME guest_rtc_ps5_title COMMAND guest_rtc_tests ps5-title) if(WIN32) - set_tests_properties(guest_rtc PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_rtc guest_rtc_ps5_title PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 4b85e9449b..17ec53064c 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -6,9 +6,12 @@ #include #include #include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" namespace { @@ -102,7 +105,20 @@ RtcDateTime fromTick(std::uint64_t tick) { static_cast(timeOfDay % TICKS_PER_MINUTE / TICKS_PER_SECOND), static_cast(timeOfDay % TICKS_PER_SECOND)}; } +bool ps4Title() { + try { + const auto titleId = GetAppTitleId_nid_postfix(); + return std::string_view(titleId.value, strnlen(titleId.value, sizeof(titleId.value))).substr(0, 4) == "CUSA"; + } catch (const std::runtime_error&) { + return false; + } catch (const std::length_error&) { + return false; + } +} + bool rangeChecked() { + static const bool ps4 = ps4Title(); + if (!ps4) return true; const auto* process = static_cast(ApplicationProcessParameters_nid_no_patch()); std::uint32_t version; std::memcpy(&version, process + 16, sizeof(version)); diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 1d72b948e4..09d0659f5f 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -7,7 +7,12 @@ #include #include #include +#include #include +#include +#include +#include +#include #include #include @@ -120,10 +125,36 @@ static void SetCompiledSdkVersion(std::byte* process, std::uint32_t version) { std::memcpy(process + 16, &version, sizeof(version)); } -int main() { +static void EnterTitleDirectory(const char* titleId) { + const auto directory = std::filesystem::temp_directory_path() / ("rtc-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + std::filesystem::create_directories(directory / "app0" / "sce_sys"); + std::filesystem::current_path(directory); + if (titleId == nullptr) return; + struct Entry { std::uint16_t key, format; std::uint32_t length, maximum, offset; }; + const std::uint32_t header[5] = {0x46535000, 0x0101, 52, 72, 2}; + const Entry entries[2] = {{0, 0x0204, 8, 8, 0}, {6, 0x0204, 10, 12, 8}}; + std::ofstream file("app0/sce_sys/param.sfo", std::ios::binary); + file.write(reinterpret_cast(header), sizeof(header)); + file.write(reinterpret_cast(entries), sizeof(entries)); + file << std::string("TITLE\0TITLE_ID\0\0\0\0\0\0", 20) << std::string("Example\0", 8) << titleId << std::string("\0\0\0", 3); +} + +int main(int argc, char** argv) { static std::array process{}; Require(ProcessParametersFixture::Install(process.data(), process.size())); SetCompiledSdkVersion(process.data(), 0x01000051); + if (argc == 2 && std::string_view(argv[1]) == "ps5-title") { + EnterTitleDirectory(nullptr); + RtcDateTime untouched{0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaa, 0xaaaaaaaau}; + RtcDateTime converted = untouched; + Require(sceRtcGetDayOfWeek(10000, 1, 1) == static_cast(0x80B50008) && sceRtcGetDayOfWeek(0, 13, 1) == static_cast(0x80B50009)); + Require(sceRtcSetTime_t(&converted, -1) == static_cast(0x80B50003) && Equal(converted, untouched)); + char text[64]; + RtcTick tick{std::numeric_limits::max()}; + Require(sceRtcFormatRFC3339(text, &tick, 1) == static_cast(0x80B50003)); + return 0; + } + EnterTitleDirectory("CUSA00000"); constexpr int invalidPointer = static_cast(0x80B50002); constexpr int invalidValue = static_cast(0x80B50003); constexpr int badParse = static_cast(0x80B50007); diff --git a/docs/dev/debt/libSceRtc.md b/docs/dev/debt/libSceRtc.md index dbdbc7f26d..12645457b4 100644 --- a/docs/dev/debt/libSceRtc.md +++ b/docs/dev/debt/libSceRtc.md @@ -8,7 +8,7 @@ - [sceRtcParseDateTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - a blank zone after seconds is accepted. With fully allocated input storage on PS5 firmware 13.42 and again on 13.60, the same NUL-terminated text returned `BAD_PARSE` when the byte after its terminator was zero and succeeded when that byte was uppercase `T`. This dependence on storage after the terminator is not modeled; controlled backing-byte measurements in a title process are unavailable. - [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71, not on a PS5: the RFC 2822 text, `INVALID_YEAR` when the zone-shifted tick is past 9999 as a signed value, otherwise `INVALID_VALUE` when the source tick or the zone-shifted tick is negative as a signed value, and the local-time variants using the offset in effect at the given instant (+01:00 in January, +02:00 in July on a Central European console). A null tick formatting the current time and the ±1439 minute offset limit come from the PS5 measurement in the next entry and are not measured on the PS4 -- [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. +- [sceRtcFormatRFC3339, sceRtcGetDayOfWeek, sceRtcSetTime_t](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - measured on PS4 firmware 10.71 from processes compiled with SDK versions 0x01000051, 0x02ffffff, 0x03000000 and 0x05050001, not on a PS5. From 0x03000000 on: `FormatRFC3339` returns `INVALID_VALUE` when adding the zone shift to the tick overflows or underflows 64 bits (earlier SDKs wrap); `GetDayOfWeek` checks the month before the year and rejects a year past 9999 (earlier SDKs check the year first, with no upper bound); `SetTime_t` rejects negative seconds with `INVALID_VALUE` and converts all 64 bits (earlier SDKs take the low 32 bits). 0x02ffffff still behaves the earlier way and 0x03000000 the later way. The earlier rules apply only to a PS4 title (a CUSA title ID): a PS5 title has its own SDK numbering, so it always gets the later rules, the behaviour main had before; those are not measured on a PS5. - [sceRtcGetDosTime, sceRtcSetDosTime](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). A year outside 1980-2107 makes `GetDosTime` return `INVALID_YEAR` after writing 0 for an earlier one and `0xff9fbf7d` (2107-12-31 23:59:58, the largest encodable time) for a later one, and `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107) without validating the fields, as the disassembly-derived [fpPS4](https://github.com/red-prig/fpPS4) reimplementation of the PS4 library does; not confirmed on a PS5 title, and shadPS4 instead masks out-of-range years into the packed value and sign-extends the 16-bit date - [sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../../core/libs/prx/libSceRtc/Export.cpp): the native wrappers obtain the zone from a kernel timezone query, while the host uses the offset at the supplied instant. The kernel timezone configuration and its failure paths are not modeled. Native formatting confirms that null tick pointers select current time, RFC3339 uses two fractional digits, and explicit offsets are limited to -1439 through 1439 minutes. - [sceRtcGetCurrentRawNetworkTick](../../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - the console reads clock 0x13 and returns its time plus the Unix epoch in ticks, or `0x80B50001` when the clock is unsupported (errno 5). The raw network clock is not modelled; this returns the host clock. Read from the disassembly of libSceRtc.sprx on firmware 13.60, not measured on a console From 0f6fc31c98c279aa27569d6c22d8533297bd9a91 Mon Sep 17 00:00:00 2001 From: emirplus Date: Fri, 23 Oct 2026 19:01:50 +0300 Subject: [PATCH 249/318] fix(tools): reconfigure streams with backslashreplace in check_skips --- tools/check_skips.py | 2 +- tools/tests/test_check_skips.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tools/check_skips.py b/tools/check_skips.py index e44304872d..d29c91b065 100644 --- a/tools/check_skips.py +++ b/tools/check_skips.py @@ -47,7 +47,7 @@ def skips(path): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="fail when a test is skipped that the job's device is not expected to skip") parser.add_argument("junit", type=Path, help="the file written by ctest --output-junit") parser.add_argument("--job", choices=("linux", "windows"), default="linux") diff --git a/tools/tests/test_check_skips.py b/tools/tests/test_check_skips.py index d0b03c0742..6e3871733b 100644 --- a/tools/tests/test_check_skips.py +++ b/tools/tests/test_check_skips.py @@ -5,6 +5,7 @@ import subprocess import sys import tempfile import unittest +from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import check_skips @@ -218,6 +219,33 @@ class CheckSkipsTests(unittest.TestCase): code = check_skips.main([str(junit), "--job", "linux"]) self.assertEqual(code, 0) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with mock.patch.object(check_skips.sys, "stdout", out), mock.patch.object(check_skips.sys, "stderr", err): + try: + check_skips.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + From 69708e0e804948f3a44e66170683b3d1454a4b25 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:02:43 +0000 Subject: [PATCH 250/318] fix(tools): implement write on web release test streams --- tools/tests/test_web_release.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index e48e3fd3c7..fe6d45076f 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -420,6 +420,12 @@ class TestWebRunnerTests(unittest.TestCase): self.encoding = encoding self.errors = errors + def write(self, text): + return len(text) + + def flush(self): + pass + out, err = Stream(), Stream() with patch.object(test_web.sys, "stdout", out), patch.object(test_web.sys, "stderr", err): with self.assertRaises(SystemExit): From 685e15d034e67421525cf65765eecce1e0ffb7c6 Mon Sep 17 00:00:00 2001 From: emirplus Date: Fri, 23 Oct 2026 19:03:09 +0300 Subject: [PATCH 251/318] fix(tools): reconfigure streams with backslashreplace in check_test_registration --- tools/check_test_registration.py | 2 +- tools/tests/test_test_registration.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tools/check_test_registration.py b/tools/check_test_registration.py index 7903e7262c..e74e2f72e6 100644 --- a/tools/check_test_registration.py +++ b/tools/check_test_registration.py @@ -160,7 +160,7 @@ def unrun_scripts(root): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="fail when a test executable is built that no ctest test runs") parser.add_argument("build", type=Path, nargs="?", default=Path("build"), help="configured build directory") parser.add_argument("--list", action="store_true", help="print every test executable and whether ctest runs it") diff --git a/tools/tests/test_test_registration.py b/tools/tests/test_test_registration.py index c6bc83ae2b..34cf8c65d5 100644 --- a/tools/tests/test_test_registration.py +++ b/tools/tests/test_test_registration.py @@ -394,6 +394,33 @@ class CheckRegistrationCliTests(unittest.TestCase): code = check_test_registration.main(["--scripts"]) self.assertEqual(code, 0) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(check_test_registration.sys, "stdout", out), patch.object(check_test_registration.sys, "stderr", err): + try: + check_test_registration.main(["--invalid-argument"]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() From b0e3c611054b63062c2d884ee8881ef02230d03b Mon Sep 17 00:00:00 2001 From: emirplus Date: Fri, 23 Oct 2026 19:04:18 +0300 Subject: [PATCH 252/318] fix(tools): reconfigure streams with backslashreplace in configure_seed --- tools/configure_seed.py | 2 +- tools/tests/test_configure_seed.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tools/configure_seed.py b/tools/configure_seed.py index 8e042f9722..4e0a82372c 100644 --- a/tools/configure_seed.py +++ b/tools/configure_seed.py @@ -28,7 +28,7 @@ def quote(value): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description="Write the configure check results of a CMake cache as an initial cache file") parser.add_argument("cache", help="CMakeCache.txt of a configured tree") parser.add_argument("seed", help="file to pass to cmake -C") diff --git a/tools/tests/test_configure_seed.py b/tools/tests/test_configure_seed.py index 785b724136..ec7253a1fe 100644 --- a/tools/tests/test_configure_seed.py +++ b/tools/tests/test_configure_seed.py @@ -4,6 +4,7 @@ import subprocess import sys import tempfile import unittest +from unittest import mock from pathlib import Path TOOL = Path(__file__).resolve().parents[1] / "configure_seed.py" @@ -229,6 +230,33 @@ class ChecksParsingTests(unittest.TestCase): found = configure_seed.checks(cache) self.assertEqual(found, {"FIRST_CHECK": ("1", "Test FIRST_CHECK")}) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with mock.patch.object(configure_seed.sys, "stdout", out), mock.patch.object(configure_seed.sys, "stderr", err): + try: + configure_seed.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() From 082a799e7e9396fc10a094b891beb3220876e971 Mon Sep 17 00:00:00 2001 From: emirplus Date: Fri, 23 Oct 2026 19:06:52 +0300 Subject: [PATCH 253/318] fix(tools): reconfigure streams with backslashreplace in build_web --- tools/build_web.py | 2 +- tools/tests/test_build_web.py | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tools/build_web.py b/tools/build_web.py index ed871bb1d4..8a5e555bf0 100644 --- a/tools/build_web.py +++ b/tools/build_web.py @@ -167,7 +167,7 @@ def publish_release(output): def main(argv=None): for stream in (sys.stdout, sys.stderr): if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8") + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser() parser.add_argument("--build", type=Path, required=True) parser.add_argument("--release", "--libraries", dest="release", action="store_true", diff --git a/tools/tests/test_build_web.py b/tools/tests/test_build_web.py index 91e093b70d..11c38bfdd4 100644 --- a/tools/tests/test_build_web.py +++ b/tools/tests/test_build_web.py @@ -149,6 +149,33 @@ class BuildWebCliTests(unittest.TestCase): code = build_web.main(["--build", str(build)]) self.assertEqual(code, 0) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with patch.object(build_web.sys, "stdout", out), patch.object(build_web.sys, "stderr", err): + try: + build_web.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() From dfacf431f6564727edce910ca04d0732064c50f4 Mon Sep 17 00:00:00 2001 From: emirplus Date: Fri, 23 Oct 2026 19:08:36 +0300 Subject: [PATCH 254/318] fix(tools): wrap entrypoint in main with stream reconfigure in trace_digest --- tools/tests/test_trace_digest.py | 28 ++++++++++++++++++++++++++++ tools/trace_digest.py | 6 +++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tools/tests/test_trace_digest.py b/tools/tests/test_trace_digest.py index 7df5e64fae..8099ca9eee 100644 --- a/tools/tests/test_trace_digest.py +++ b/tools/tests/test_trace_digest.py @@ -7,6 +7,7 @@ import subprocess import sys import tempfile import unittest +from unittest import mock from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) @@ -744,6 +745,33 @@ class IntervalDigestTests(unittest.TestCase): loaded = trace_digest.load_summary(str(summary_file), SUMMARY["trace"]) self.assertEqual(loaded["commit"], SUMMARY["commit"]) + def test_main_reconfigures_streams(self): + class Stream: + def __init__(self): + self.encoding = None + self.errors = None + + def reconfigure(self, encoding=None, errors=None): + self.encoding = encoding + self.errors = errors + + def write(self, text): + pass + + def flush(self): + pass + + out, err = Stream(), Stream() + with mock.patch.object(trace_digest.sys, "stdout", out), mock.patch.object(trace_digest.sys, "stderr", err): + try: + trace_digest.main([]) + except SystemExit: + pass + self.assertEqual(out.encoding, "utf-8") + self.assertEqual(out.errors, "backslashreplace") + self.assertEqual(err.encoding, "utf-8") + self.assertEqual(err.errors, "backslashreplace") + if __name__ == "__main__": unittest.main() diff --git a/tools/trace_digest.py b/tools/trace_digest.py index d259fbb62d..59c9fb3bf9 100644 --- a/tools/trace_digest.py +++ b/tools/trace_digest.py @@ -344,6 +344,9 @@ def nonnegative_integer(value): def main(arguments=None): + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + stream.reconfigure(encoding="utf-8", errors="backslashreplace") parser = argparse.ArgumentParser(description=DESCRIPTION) parser.add_argument("trace", help="aps5-trace-*.json.gz (or .json), or a .jsonl summary file with APS5_TRACE_SUMMARY_EVERY lines") parser.add_argument("--summary", help="file with the summary line (default: aps5-summary.jsonl next to the trace)") @@ -378,9 +381,6 @@ def main(arguments=None): if __name__ == "__main__": - for stream in (sys.stdout, sys.stderr): - if hasattr(stream, "reconfigure"): - stream.reconfigure(encoding="utf-8", errors="replace") try: sys.exit(main()) except (EOFError, OSError, ValueError, zlib.error) as error: From dd9716e1c3329d22c5e77dbdb42fb79507f113ad Mon Sep 17 00:00:00 2001 From: orezraey Date: Thu, 22 Oct 2026 23:09:37 -0300 Subject: [PATCH 255/318] fix(libSceNet): convert sceNetInetPtonEx without an output as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 5 ++++- core/libs/tests/GuestSceNet.cpp | 5 ++++- docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index f56153435c..27a3eb1bca 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -2474,13 +2474,16 @@ int APS5_VABI sceNetInetPtonEx(int af, const char* src, void* dst, int) { if (af != NET_AF_INET) { return sceNetInetPton(af, src, dst); } - if (src == nullptr || dst == nullptr) { + if (src == nullptr) { return fail(NET_EINVAL); } std::uint32_t address = 0; if (!parse_ipv4_parts(src, address)) { return 0; } + if (dst == nullptr) { + return 1; + } const std::array bytes{static_cast(address >> 24), static_cast(address >> 16), static_cast(address >> 8), static_cast(address)}; std::memcpy(dst, bytes.data(), bytes.size()); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 276da969b7..058be81bc4 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -984,7 +984,10 @@ static void CheckInetPtonEx() { Require(sceNetInetPtonEx(2, "not an address", address.data(), 0) == 0); Require(Failed(sceNetInetPtonEx(99, "127.0.0.1", address.data(), 0), 47)); Require(Failed(sceNetInetPtonEx(2, nullptr, address.data(), 0), 22)); - Require(Failed(sceNetInetPtonEx(2, "127.0.0.1", nullptr, 0), 22)); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPtonEx(2, "127.0.0.1", nullptr, 0) == 1 && *sceNetErrnoLoc() == 123); + Require(sceNetInetPtonEx(2, "abc", nullptr, 0) == 0 && *sceNetErrnoLoc() == 123); + Require(sceNetInetPtonEx(2, "0x", address.data(), 0) == 1 && address[0] == 0 && address[1] == 0 && address[2] == 0 && address[3] == 0); for (const int flags : {1, 0x100, -1}) { address.fill(0); Require(sceNetInetPtonEx(2, "1.2.3", address.data(), flags) == 1); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 625feb8f4e..781b0b6276 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -15,7 +15,7 @@ - [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII digit bytes throw because their decoding is unmeasured; other firmware versions remain unverified. Fixed character pairs, ignored separators, alphabetic values beyond f, non-alphanumeric ASCII values of 0x7f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) - [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. -- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp); IPv4 conversion and the ignored flags follow PS4 firmware 10.71, not yet confirmed on PS5. There IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at the first space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails. Flags 0, each single bit up to 0x100, 0x10000, 0x80000000 and -1 gave the same results; other values were not measured. LF, FF and CR as terminators, and `NET_EINVAL` for a null `src` or `dst`, follow `sceNetInetPton` and were not measured for this function. IPv6 converts as `sceNetInetPton` does. +- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - for IPv6 a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60); flag values other than 0, single bits up to 0x100, 0x10000, 0x80000000 and -1 were not measured. The `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp). On PS4 firmware 10.71 and on that PS5 (26 inputs, six flag values), IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at a space, tab, LF, VT, FF or CR), except that a part starting with `09` is decimal and an octal part with `8` or `9` fails; flags are ignored; a null `src` returns `NET_EINVAL`; a null `dst` returns 1 or 0 without writing. IPv6 converts as `sceNetInetPton` does - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced: a reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents here. Flag 4 bypasses the system pool reserve threshold; with no pool there is no threshold, so it allocates as other flags do. What the console returns once the pool is exhausted is unmeasured; AnyPS5 returns null without setting an errno. Measured on a PS5 (firmware 13.60): a size of 0 or less returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 had no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) is a zero-argument, non-returning void export in the native module. The protected host stub still declares int and contains `return 0`; changing it to void requires changing that protected body, so the return-type mismatch is retained. - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. From d4321c65693574ae8582318a45f99082b7a13372 Mon Sep 17 00:00:00 2001 From: orezraey Date: Thu, 22 Oct 2026 23:16:59 -0300 Subject: [PATCH 256/318] docs(libSceNet): separate the PS4 and PS5 measurements of sceNetInetPtonEx Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestSceNet.cpp | 1 - docs/dev/debt/libSceNet.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 058be81bc4..adbbabbe7a 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -987,7 +987,6 @@ static void CheckInetPtonEx() { *sceNetErrnoLoc() = 123; Require(sceNetInetPtonEx(2, "127.0.0.1", nullptr, 0) == 1 && *sceNetErrnoLoc() == 123); Require(sceNetInetPtonEx(2, "abc", nullptr, 0) == 0 && *sceNetErrnoLoc() == 123); - Require(sceNetInetPtonEx(2, "0x", address.data(), 0) == 1 && address[0] == 0 && address[1] == 0 && address[2] == 0 && address[3] == 0); for (const int flags : {1, 0x100, -1}) { address.fill(0); Require(sceNetInetPtonEx(2, "1.2.3", address.data(), flags) == 1); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 781b0b6276..78cb581563 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -15,7 +15,7 @@ - [sceNetEtherStrton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - strings shorter than 17 bytes return EINVAL without changing the output, rather than reading past the terminating NUL as in the [firmware 13.60 probe](https://gist.github.com/orezraey/789290577123af3c45b340d6db6f395c/71e0a6215c7ee86f49fdb31de989dd2b50720b7a). Non-ASCII digit bytes throw because their decoding is unmeasured; other firmware versions remain unverified. Fixed character pairs, ignored separators, alphabetic values beyond f, non-alphanumeric ASCII values of 0x7f, and null-pointer EINVAL follow those measurements, not the strict [shadPS4 parser](https://github.com/shadps4-emu/shadPS4/blob/d615b3887ba29025a59242e1a26f5b68bb176158/src/core/libraries/net/net.cpp#L1274-L1297) - [sceNetGetDnsInfo](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the `(info, int flags)` signature, the two IPv4 server slots in network byte order and the server count as the return value follow the PS4 [OpenOrbis musl resolver](https://github.com/OpenOrbis/musl/blob/2b0bb8ce2fa64ac255cb656785729d159a5f074c/src/network/resolvconf.c) and [OpenOrbis Net.h](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9dd4a92695538bdeb09fb056d06dd11725/include/orbis/Net.h); unverified on PS5 hardware. It reports the host DNS servers of the interface `sceNetCtlGetInfo` reports, never servers set through `sceNetSetDnsInfo`. Zeroing the unused slots and returning 0 without a host IPv4 interface are unverified. Nonzero flags and a null info throw because their console results are unknown - [sceNetGetRandom](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - generates values with the host random device; the system library maintains its own seeded PRNG state. -- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - for IPv6 a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60); flag values other than 0, single bits up to 0x100, 0x10000, 0x80000000 and -1 were not measured. The `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp). On PS4 firmware 10.71 and on that PS5 (26 inputs, six flag values), IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at a space, tab, LF, VT, FF or CR), except that a part starting with `09` is decimal and an octal part with `8` or `9` fails; flags are ignored; a null `src` returns `NET_EINVAL`; a null `dst` returns 1 or 0 without writing. IPv6 converts as `sceNetInetPton` does +- [sceNetInetPtonEx](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - for IPv6 a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60). The `(int af, const char* src, void* dst, int flags)` signature follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp). On PS4 firmware 10.71 and on that PS5, IPv4 text is read as `inet_aton` reads it (`a`, `a.b`, `a.b.c`, hex and octal parts, values wrapping at 32 bits, the text ending at a space, tab or VT), except that a part starting with `09` is decimal (`"0989"` is 989) and an octal part with `8` or `9` fails; flags were ignored (PS4: 0, single bits up to 0x100, 0x10000, 0x80000000, -1; PS5: 1, 2, 4, 0x100, 0x10000, -1). On the PS5 only: LF, FF and CR also end the text, a null `src` returns `NET_EINVAL`, and a null `dst` returns 1 or 0 without writing or setting errno. IPv6 converts as `sceNetInetPton` does - [sceNetMemoryAllocate and sceNetMemoryFree](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - allocations use the guest heap instead of the system network pool. Pool limits, its allocation accounting and free-time fill are not reproduced: a reused block allocated without flag 2 holds 0xdd bytes on the console and its previous contents here. Flag 4 bypasses the system pool reserve threshold; with no pool there is no threshold, so it allocates as other flags do. What the console returns once the pool is exhausted is unmeasured; AnyPS5 returns null without setting an errno. Measured on a PS5 (firmware 13.60): a size of 0 or less returns null and leaves `sceNetErrnoLoc` unchanged, flag 2 zero-fills, flags 1, 4, 8, 0x10, 0x100 and 0x80000000 had no visible effect, and every block of 1 to 65536 bytes was 64-byte aligned - [sceNetThreadExit](../../../core/libs/prx/libSceNet/Export.cpp) is a zero-argument, non-returning void export in the native module. The protected host stub still declares int and contains `return 0`; changing it to void requires changing that protected body, so the return-type mismatch is retained. - [sceNetConfigCleanUpAllInterfaces](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - the native routine consumes no arguments; the throwing implementation remains unchanged. From b2dc84b68daa2c1e1b3718858cb890da94d41f92 Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 13:17:24 -0300 Subject: [PATCH 257/318] fix(libSceNet): read sceNetInetPton text and null outputs as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 24 ++++++++++++++++++------ core/libs/tests/GuestSceNet.cpp | 17 ++++++++++++++++- docs/dev/debt/libSceNet.md | 2 +- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 27a3eb1bca..29efb3e552 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -1962,20 +1962,32 @@ uint32_t APS5_VABI sceNetNtohl_nid_postfix(uint32_t net32) { return swap32(net32 uint16_t APS5_VABI sceNetNtohs_nid_postfix(uint16_t net16) { return swap16(net16); } int APS5_VABI sceNetInetPton(int af, const char* src, void* dst) { - if (src == nullptr || dst == nullptr) { + if (src == nullptr) { return fail(NET_EINVAL); } if (af != NET_AF_INET && af != NET_AF_INET6) { return fail(NET_EAFNOSUPPORT); } - const std::string text = af == NET_AF_INET ? std::string(src, std::strcspn(src, " \t\n\v\f\r")) : std::string(src); - std::array address{}; + if (dst == nullptr && af == NET_AF_INET6) { + return fail(NET_EINVAL); + } + const auto convert = [](int family, const std::string& text, unsigned char* address) { #ifdef _WIN32 - const int converted = InetPtonA(af == NET_AF_INET ? AF_INET : AF_INET6, text.c_str(), address.data()); + return InetPtonA(family, text.c_str(), address); #else - const int converted = ::inet_pton(af == NET_AF_INET ? AF_INET : AF_INET6, text.c_str(), address.data()); + return ::inet_pton(family, text.c_str(), address); #endif - if (converted == 1) { + }; + const std::string head(src, std::strcspn(src, " \t\n\v\f\r")); + const auto colon = head.rfind(':'); + const bool dottedTail = af == NET_AF_INET6 && colon != std::string::npos && head.find('.', colon) != std::string::npos; + std::array address{}; + if (dottedTail && convert(AF_INET, head.substr(colon + 1), address.data()) != 1) { + return 0; + } + const std::string text = af == NET_AF_INET || dottedTail ? head : std::string(src); + const int converted = convert(af == NET_AF_INET ? AF_INET : AF_INET6, text, address.data()); + if (converted == 1 && dst != nullptr) { std::memcpy(dst, address.data(), af == NET_AF_INET ? 4 : 16); } return converted; diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index adbbabbe7a..ad4e23494a 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -926,9 +926,24 @@ static void CheckInetPtonWhitespace() { for (const char* text : {" 1.2.3.4", "1.2.3.4x", "1.2.3.4.", "1.2.3.4\x01", "1.2.3.4\xa0", "1.2. 3.4", "1.2.3", "1.2.3 ", "01.2.3.4", "256.0.0.1"}) { CheckInetPtonRejects(2, text); } - for (const char* text : {"::1 ", "::1\t", " ::1", "::1x"}) { + for (const char* text : {"::1 ", "::1\t", " ::1", "::1x", "::ffff:1.2.3.4x", "::ffff:1.2.3.4\x01", "::ffff:01.2.3.4", "::ffff:1.2.3", + "::ffff:0x1.2.3.4", "::ffff ", "::ffff:1.2 .3.4"}) { CheckInetPtonRejects(28, text); } + for (const char* text : {"::ffff:1.2.3.4 x", "::ffff:1.2.3.4\t", "::ffff:1.2.3.4\nzz", "::ffff:1.2.3.4 "}) { + std::array address{}; + *sceNetErrnoLoc() = 123; + Require(sceNetInetPton(28, text, address.data()) == 1 && *sceNetErrnoLoc() == 123); + Require(address[10] == 0xff && address[11] == 0xff && address[12] == 1 && address[15] == 4); + } + std::array full{}; + Require(sceNetInetPton(28, "1:2:3:4:5:6:1.2.3.4 z", full.data()) == 1 && full[1] == 1 && full[11] == 6 && full[12] == 1 && full[15] == 4); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPton(2, "1.2.3.4", nullptr) == 1 && *sceNetErrnoLoc() == 123); + Require(sceNetInetPton(2, "abc", nullptr) == 0 && *sceNetErrnoLoc() == 123); + Require(sceNetInetPton(28, "::1", nullptr) < 0 && *sceNetErrnoLoc() == 22); + *sceNetErrnoLoc() = 123; + Require(sceNetInetPton(28, nullptr, full.data()) < 0 && *sceNetErrnoLoc() == 22); } struct AtonCase { diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index 78cb581563..b9ad83d736 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -45,6 +45,6 @@ - [sceNetRecv, sceNetRecvfrom and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) on a datagram socket with no local address wait on Windows and macOS until a bind, connect or send gives it one, checking the host socket every 10 ms. Winsock fails such a receive with `WSAEINVAL`, where FreeBSD 11.4's `soreceive_dgram` waits for a datagram (`sys/kern/uipc_socket.c`). On macOS the host receive waits, but a receive-side `shutdown` does not wake it. A non-blocking socket fails with `EAGAIN` at once, and the receive timeout ends the wait with `EAGAIN`; when the address comes first, the call waits for a datagram only for what is left of the timeout. A `sceNetShutdown` of the receive side ends the wait and the receive returns 0, and a zero-length receive returns 0 without waiting, as `soreceive_dgram` does when the socket can't receive more or nothing was asked for. An abort or close ends the wait, and the host receive then fails as it did before - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does - [sceNetResolverStartNtoa6](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) resolves a host name to its first IPv6 address through the host resolver (`getaddrinfo` with `AF_INET6`) and writes the 16-byte address; the `(rid, hostname, addr, timeout, retry, flags)` signature follows `sceNetResolverStartNtoa`. A name with no IPv6 address fails with `RESOLVER_ENODNS` (`0x804101E1`) like a failed lookup; whether the console returns IPv4-mapped addresses instead is not verified. Timeout and retry are ignored as in `sceNetResolverStartNtoa`, and any nonzero flags throw because their meaning for this call is not known. Unverified on hardware. -- [sceNetInetPton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) ends an IPv4 address at the first space, tab, LF, VT, FF or CR and ignores the rest (`"1.2.3.4 x"` converts), leaves the destination and `sceNetErrnoLoc` unchanged when a conversion fails, as PS4 firmware 10.71 does; not yet confirmed on PS5. Leading whitespace, any other trailing byte (`0x01`, `0xa0`) and IPv6 text with whitespace fail. +- [sceNetInetPton](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - an IPv6 conversion with a null `dst` returns `NET_EINVAL` here, where it crashed a payload process on a PS5 (firmware 13.60). On that PS5 and on PS4 firmware 10.71 an IPv4 address ends at the first space, tab, LF, VT, FF or CR and the rest is ignored (`"1.2.3.4 x"` converts), and a failed conversion leaves the destination and `sceNetErrnoLoc` unchanged. On the PS5 the dotted IPv4 part of IPv6 text is read the same way (`"::ffff:1.2.3.4 x"` converts, `"::ffff:01.2.3.4"` fails), and an IPv4 conversion with a null `dst` returns 1 or 0 without writing. Leading whitespace, any other trailing byte (`0x01`, `0xa0`) and whitespace after hexadecimal IPv6 text fail - [sceNetDhcpStart, sceNetAddrConfig6Start, sceNetDuplicateIpStart and sceNetInfoDumpStart](../../../core/libs/prx/libSceNet/Export.cpp) - argument widths and callback ABI are recovered. Address callbacks receive user data, a 32-bit event and a 32-bit error. DHCP configuration is mutable; the 512-byte IPv6 configuration is read-only. Their complete field layouts remain unknown. The two 32-bit thread parameters are stored but not consumed by this firmware thread wrapper, so their original names and units are unconfirmed. Information-dump options contain flags, an interval in seconds and a 64-byte path. All four exports remain unimplemented. From 1bb17028061e7f4d149138c6672145c89db15530 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:18:15 +0000 Subject: [PATCH 258/318] test(tools): assert web release parse error status --- tools/tests/test_web_release.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/tests/test_web_release.py b/tools/tests/test_web_release.py index fe6d45076f..d67632e117 100644 --- a/tools/tests/test_web_release.py +++ b/tools/tests/test_web_release.py @@ -428,8 +428,9 @@ class TestWebRunnerTests(unittest.TestCase): out, err = Stream(), Stream() with patch.object(test_web.sys, "stdout", out), patch.object(test_web.sys, "stderr", err): - with self.assertRaises(SystemExit): + with self.assertRaises(SystemExit) as error: test_web.main([]) + self.assertEqual(error.exception.code, 2) self.assertEqual(out.encoding, "utf-8") self.assertEqual(out.errors, "backslashreplace") self.assertEqual(err.encoding, "utf-8") From 1852888af9234ddce6b8aec9a7a38811e3623727 Mon Sep 17 00:00:00 2001 From: Project-MOC <83237812+Project-MOC@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:27:37 +0200 Subject: [PATCH 259/318] fix(tools): preserve SELF outputs on failed writes --- tools/tests/test_unwrap_self.py | 149 ++++++++++++++++++++++++++++++++ tools/unwrap_self.py | 21 +++-- 2 files changed, 164 insertions(+), 6 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index f30eb5a74f..648d5471b2 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -1,4 +1,5 @@ import contextlib +import errno import io import os import struct @@ -116,6 +117,154 @@ class UnwrapTest(unittest.TestCase): self.assertEqual(code, 0) self.assertTrue(output.exists()) + def test_write_failure_preserves_output_and_removes_temporary_files(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + container = build_container(default_segments(), default_records(), default_payloads()) + source.write_bytes(container) + real_open = io.open + + def failing_open(path, mode="r", *args, **kwargs): + stream = real_open(path, mode, *args, **kwargs) + if "w" not in mode: + return stream + writer = mock.MagicMock(wraps=stream) + writer.__enter__.return_value = writer + writer.__exit__.side_effect = lambda *args: stream.close() + + def fail_after_partial_write(data): + stream.write(data[:16]) + stream.flush() + raise OSError(errno.ENOSPC, "simulated write failure") + + writer.write.side_effect = fail_after_partial_write + return writer + + for previous in (None, b"previous output"): + with self.subTest(previous=previous): + output.unlink(missing_ok=True) + if previous is not None: + output.write_bytes(previous) + before = set(directory.iterdir()) + errors = io.StringIO() + with mock.patch("io.open", side_effect=failing_open), contextlib.redirect_stderr(errors): + status = unwrap_self.main([str(source), str(output)]) + self.assertEqual(status, 2) + self.assertIn("simulated write failure", errors.getvalue()) + self.assertEqual(set(directory.iterdir()), before) + if previous is not None: + self.assertEqual(output.read_bytes(), previous) + self.assertEqual(source.read_bytes(), container) + + def test_replace_failure_preserves_output_and_removes_temporary_files(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + container = build_container(default_segments(), default_records(), default_payloads()) + source.write_bytes(container) + output.write_bytes(b"previous output") + before = set(directory.iterdir()) + errors = io.StringIO() + with mock.patch.object(Path, "replace", side_effect=OSError("simulated replacement failure")), \ + contextlib.redirect_stderr(errors), contextlib.redirect_stdout(io.StringIO()): + status = unwrap_self.main([str(source), str(output)]) + self.assertEqual(status, 2) + self.assertIn("simulated replacement failure", errors.getvalue()) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(source.read_bytes(), container) + self.assertEqual(set(directory.iterdir()), before) + + def test_successful_conversion_replaces_existing_output_without_temporary_files(self): + container = build_container(default_segments(), default_records(), default_payloads()) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + expected = output.read_bytes() + before = set(Path(self.directory).iterdir()) + output.write_bytes(b"previous output") + with contextlib.redirect_stdout(io.StringIO()): + status = unwrap_self.main([str(Path(self.directory) / "input.self"), str(output)]) + self.assertEqual(status, 0) + self.assertEqual(output.read_bytes(), expected) + self.assertEqual(set(Path(self.directory).iterdir()), before) + + def test_close_failure_preserves_existing_output(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + source.write_bytes(build_container(default_segments(), default_records(), default_payloads())) + output.write_bytes(b"previous output") + before = set(directory.iterdir()) + real_open = io.open + + def failing_open(path, mode="r", *args, **kwargs): + stream = real_open(path, mode, *args, **kwargs) + if "w" not in mode: + return stream + writer = mock.MagicMock(wraps=stream) + writer.__enter__.return_value = writer + + def fail_on_close(*args): + stream.close() + raise OSError("simulated close failure") + + writer.__exit__.side_effect = fail_on_close + return writer + + errors = io.StringIO() + with mock.patch("io.open", side_effect=failing_open), contextlib.redirect_stderr(errors): + status = unwrap_self.main([str(source), str(output)]) + self.assertEqual(status, 2) + self.assertIn("simulated close failure", errors.getvalue()) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(set(directory.iterdir()), before) + + def test_temporary_file_creation_failure_preserves_existing_output(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + source.write_bytes(build_container(default_segments(), default_records(), default_payloads())) + output.write_bytes(b"previous output") + before = set(directory.iterdir()) + errors = io.StringIO() + with mock.patch.object(tempfile, "NamedTemporaryFile", side_effect=OSError("simulated creation failure")), \ + contextlib.redirect_stderr(errors): + status = unwrap_self.main([str(source), str(output)]) + self.assertEqual(status, 2) + self.assertIn("simulated creation failure", errors.getvalue()) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(set(directory.iterdir()), before) + + @unittest.skipUnless(os.name == "posix", "POSIX permission bits") + def test_replacing_output_preserves_existing_permissions(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + source.write_bytes(build_container(default_segments(), default_records(), default_payloads())) + output.write_bytes(b"previous output") + output.chmod(0o640) + unwrap_self.unwrap(source, output) + self.assertTrue(output.read_bytes().startswith(b"\x7fELF")) + self.assertEqual(output.stat().st_mode & 0o777, 0o640) + + def test_output_symlink_is_kept_and_its_target_is_replaced(self): + container = build_container(default_segments(), default_records(), default_payloads()) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + expected = output.read_bytes() + output.unlink() + target = Path(self.directory) / "target.elf" + target.write_bytes(b"previous output") + try: + output.symlink_to(target.name) + except OSError as error: + self.skipTest(str(error)) + before = set(Path(self.directory).iterdir()) + unwrap_self.unwrap(Path(self.directory) / "input.self", output) + self.assertTrue(output.is_symlink()) + self.assertEqual(target.read_bytes(), expected) + self.assertEqual(set(Path(self.directory).iterdir()), before) + def convert(self, container, name="input.self"): directory = Path(self.directory) source = directory / name diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 659f4eb07a..26f7bfd320 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -33,6 +33,7 @@ import argparse import math import struct import sys +import tempfile from collections import Counter from pathlib import Path @@ -235,8 +236,8 @@ def check_decrypted(data, headers): def unwrap(path, output, verbose=False): - source, destination = Path(path), Path(output) - if source.resolve() == destination.resolve() or (destination.exists() and source.samefile(destination)): + source, destination = Path(path), Path(output).resolve() + if source.resolve() == destination or (destination.exists() and source.samefile(destination)): raise Refused("output aliases the input container") data = bytes(source.read_bytes()) if len(data) < FIRST_REGION: @@ -326,10 +327,18 @@ def unwrap(path, output, verbose=False): print(" [%2d] %-18s vaddr=%#011x filesz=%#010x offset=%#010x%s" % (item["index"], name, item["vaddr"], item["size"], item["new"], moved)) - destination = Path(output) - if destination.parent != Path(""): - destination.parent.mkdir(parents=True, exist_ok=True) - destination.write_bytes(result) + destination.parent.mkdir(parents=True, exist_ok=True) + temporary_path = None + try: + with tempfile.NamedTemporaryFile(mode="wb", dir=destination.parent, prefix=".unwrap-self-", delete=False) as temporary: + temporary_path = Path(temporary.name) + temporary.write(result) + if sys.platform != "win32" and destination.exists(): + temporary_path.chmod(destination.stat().st_mode) + temporary_path.replace(destination) + finally: + if temporary_path is not None: + temporary_path.unlink(missing_ok=True) return len(result) From 950a21aec8809977c39a2f47ee54c010b9694ba7 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 21 Oct 2026 01:37:26 -0400 Subject: [PATCH 260/318] fix(libkernel): return ENOENT for unknown sysctl names --- .../System/src/SystemConfiguration.cpp | 6 ++-- core/libs/tests/GuestSystemConfiguration.cpp | 33 +++++++++++++++---- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp index c0f33a4450..ff20f5f917 100644 --- a/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp +++ b/core/libs/prx/libkernel/System/src/SystemConfiguration.cpp @@ -467,7 +467,8 @@ int APS5_VABI sysctlbyname_nid_postfix(const char* name, void* oldValue, std::si static constexpr int mib[] = {6, 12}; return sysctl_nid_postfix(mib, 2, oldValue, oldLength, newValue, newLength); } - throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); + *__error_nid_postfix() = 2; + return -1; } int APS5_VABI sceKernelGetSystemSwVersion_nid_postfix(SceKernelSystemSwVersion* version) { @@ -529,6 +530,7 @@ int APS5_VABI sysctlnametomib_nid_postfix(const char* name, int* mib, std::size_ for (const auto& entry : SystemIntegers) { if (std::strcmp(name, entry.name) == 0) return copyMib(entry.mib, 2); } - throw std::runtime_error(std::string(__func__) + ": unsupported name " + name); + *__error_nid_postfix() = 2; + return -1; } } diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 707fcd5ea7..02140768bd 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -249,6 +249,29 @@ static void CheckUnansweredSysctl() { Require(size == 4 && untouched[0] == 0x5a5a5a5a && untouched[1] == 0x5a5a5a5a && untouched[2] == 0x5a5a5a5a); } } +static void CheckUnknownSysctlName() { + static constexpr const char* name = "kern.anyps5_no_such_name"; + unsigned char buffer[16]; + std::memset(buffer, 0xaa, sizeof(buffer)); + std::size_t length = sizeof(buffer); + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, buffer, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2); + Require(length == sizeof(buffer) && Untouched(buffer, sizeof(buffer))); + + length = 77; + *__error_nid_postfix() = 13; + Require(sysctlbyname_nid_postfix(name, nullptr, &length, nullptr, 0) == -1 && *__error_nid_postfix() == 2 && length == 77); + + int mib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t mibLength = 4; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(name, mib, &mibLength) == -1 && *__error_nid_postfix() == 2); + Require(mibLength == 4 && mib[0] == 0x5a5a5a5a && mib[1] == 0x5a5a5a5a && mib[2] == 0x5a5a5a5a && mib[3] == 0x5a5a5a5a); + + mibLength = 0; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(name, nullptr, &mibLength) == -1 && *__error_nid_postfix() == 2 && mibLength == 0); +} static void CheckPathnameSysctl() { *__error_nid_postfix() = 13; char buffer[32] = {}; @@ -793,13 +816,8 @@ static void CheckNameToMib() { threw = true; } Require(threw); - threw = false; - try { - sysctlnametomib_nid_postfix("kern.cp_times", mib, &size); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix("kern.anyps5_no_such_name", mib, &size) == -1 && *__error_nid_postfix() == 2); } int main() { CheckProcessorCountSysctl(); @@ -811,6 +829,7 @@ int main() { #endif CheckTscFrequencySysctl(); CheckUnansweredSysctl(); + CheckUnknownSysctlName(); CheckPathnameSysctl(); CheckSystemStringSysctl(); CheckSystemStringNames(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 515c8d8c2b..23249fc88f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -183,7 +183,7 @@ - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [kern.proc.env](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - which variables, if any, the console starts an application with is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_ENV`, pid} returns 0 bytes, the empty start-up environment `environ` and libc's `getenv` also begin with, as FreeBSD 11's [`sysctl_kern_proc_env`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/sys/kern/kern_proc.c) copies a process's start-up strings, not later `setenv` changes. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included); a write returns `EPERM`. PS5VR (PPSA99177) reads it for its own pid at start-up. - [kern.proc.vmmap](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_VMMAP`, pid} returns packed FreeBSD 11 `kinfo_vmentry` records (144 bytes each, an empty path) for the used ranges of the guest address-space arena, in address order, as [`kinfo_getvmmap`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libutil/kinfo_getvmmap.c) reads them. A range is one record: the type is always `KVME_TYPE_DEFAULT`, the protection is `sceKernelVirtualQuery`'s for the range's first page (0 where it reports none), and offset, object, resident page counts, flags and path are 0. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included, as FreeBSD's `pget` gives); a write returns `EPERM` and a short buffer `ENOMEM`. Ranges outside the arena, which guest code cannot map, are not reported. The walk takes the arena lock once per range, not for the whole walk as FreeBSD holds the map lock, so an `mmap` or `munmap` on another thread can change the list between two records. Whether a PS5 application may read its own map, and what types and paths the console reports, is unverified -- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; here `sysctlnametomib` throws for them. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`, which its `sysctlnametomib` does not resolve; other names still throw here. A title process was not measured +- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; #8553 implements the mappings. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`; here unknown names return `ENOENT` from `sysctlbyname` and `sysctlnametomib`, following the FreeBSD name lookup error. Whether a title queries an unknown name or sees the same error is unmeasured. - [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) From a05ea75447693b84f2844873fcdfeb9a90a05db4 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 21 Oct 2026 01:41:12 -0400 Subject: [PATCH 261/318] docs(libkernel): clarify unknown sysctl lookup behavior --- docs/dev/debt/libkernel.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 23249fc88f..c0804618fa 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -183,7 +183,8 @@ - [kern.proc.pathname](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) returns `/app0/eboot.bin` for pid -1 and the calling process, following FreeBSD's `sysctl_kern_proc_pathname`; the exact pathname on retail consoles remains unverified. - [kern.proc.env](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - which variables, if any, the console starts an application with is unverified; `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_ENV`, pid} returns 0 bytes, the empty start-up environment `environ` and libc's `getenv` also begin with, as FreeBSD 11's [`sysctl_kern_proc_env`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/sys/kern/kern_proc.c) copies a process's start-up strings, not later `setenv` changes. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included); a write returns `EPERM`. PS5VR (PPSA99177) reads it for its own pid at start-up. - [kern.proc.vmmap](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctl` {`CTL_KERN`, `KERN_PROC`, `KERN_PROC_VMMAP`, pid} returns packed FreeBSD 11 `kinfo_vmentry` records (144 bytes each, an empty path) for the used ranges of the guest address-space arena, in address order, as [`kinfo_getvmmap`](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libutil/kinfo_getvmmap.c) reads them. A range is one record: the type is always `KVME_TYPE_DEFAULT`, the protection is `sceKernelVirtualQuery`'s for the range's first page (0 where it reports none), and offset, object, resident page counts, flags and path are 0. Only the calling process's pid is accepted (`ESRCH` otherwise, -1 included, as FreeBSD's `pget` gives); a write returns `EPERM` and a short buffer `ENOMEM`. Ranges outside the arena, which guest code cannot map, are not reported. The walk takes the arena lock once per range, not for the whole walk as FreeBSD holds the map lock, so an `mmap` or `munmap` on another thread can change the list between two records. Whether a PS5 application may read its own map, and what types and paths the console reports, is unverified -- [sysctl names a PS5 payload process gets ENOENT for](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - `sysctlbyname` returns -1 with `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`, and `sysctl` does the same for the MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}. The buffer and the length are left alone. A payload process on a PS5 (firmware 12.70) gets this for a read, a size query, a null length and a null buffer with a null length, by name and by MIB. `sysctlnametomib` gives those seven MIBs, and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size` without touching the count or the first three words of the MIB, as the console does. A write to these names and MIBs throws, except for `machdep.cacheline_size`, which returns `ENOENT` as FreeBSD amd64 does for an OID it does not define; the console's answer to a write was not tried. The console's `sysctlnametomib` gives `kern.smp.maxcpus`, `kern.stackprot`, `kern.sched.name` and `kern.timecounter.hardware` the MIBs {1, 2147482816, 2147482814}, {1, 2147483151}, {1, 2147483182, 2147483181} and {1, 2147482838, 2147482834}, and its `sysctl` returns `ENOENT` for them; #8553 implements the mappings. `hw.physmem`, `hw.realmem` and `vm.overcommit` return `ENOENT` on the console too but report values here, as the `hw.realmem` and `vm.overcommit` entries say. A payload process also gets `ENOENT` for `kern.anyps5_no_such_name`; here unknown names return `ENOENT` from `sysctlbyname` and `sysctlnametomib`, following the FreeBSD name lookup error. Whether a title queries an unknown name or sees the same error is unmeasured. +- [sysctl names measured absent on a PS5 payload](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - On firmware 12.70, a payload gets `ENOENT` for `hw.byteorder`, `hw.cacheline_size`, `hw.machine_arch`, `hw.usermem`, `kern.clockrate`, `kern.maxfiles`, `kern.maxfilesperproc`, `kern.maxproc`, `kern.ps_strings`, `kern.sched.name`, `kern.smp.maxcpus`, `kern.stackprot`, `kern.timecounter.hardware` and `machdep.cacheline_size`. `sysctl` returns `ENOENT` for MIBs {1, 6}, {1, 7}, {1, 12}, {1, 27}, {1, 32}, {6, 4} and {6, 11}; reads and size queries leave output buffers and lengths untouched. `sysctlnametomib` resolves the seven corresponding MIB names and returns `ENOENT` for `hw.cacheline_size`, `hw.usermem` and `machdep.cacheline_size`. Writes and title behavior are unmeasured. +- [Unknown sysctl names](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) - A payload process on firmware 12.70 gets `ENOENT` for `kern.anyps5_no_such_name` through `sysctlbyname` and `sysctlnametomib`; here both name-based calls return `ENOENT` for unrecognized names, following FreeBSD name lookup. This is one payload measurement; title behavior for other unknown names is unverified. Numeric MIBs with no implementation still throw. - [hw.pagesize](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the system page size (16384 bytes, PS5_PAGE_SIZE) as an integer through sysctl MIB {6, 7} (CTL_HW, HW_PAGESIZE) and the "hw.pagesize" sysctlbyname lookup, following FreeBSD's [`hw.pagesize`](https://github.com/freebsd/freebsd-src/blob/main/sys/kern/kern_mib.c#L177), a read-only `SYSCTL_INT` of `PAGE_SIZE`. The value on a PS5 follows the 16 KiB page size the rest of libkernel uses and is unmeasured. A write returns -1 with EPERM. - [hw.model](../../../core/libs/prx/libkernel/System/src/SystemConfiguration.cpp) (libkernel) reports the [host CPU's name](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp), built as FreeBSD 11 builds `hw.model`: the CPUID brand string (leaves 0x80000002 to 0x80000004) without leading spaces ([identcpu.c](https://github.com/freebsd/freebsd-src/blob/release/11.0.0/sys/x86/x86/identcpu.c#L683-L691)). Trailing padding stays, as it does there, so an AMD host's name ends in spaces. The name a PS5 reports is unknown. A write returns -1 with `EPERM`. A host without an x86 CPUID brand string, such as an Apple Silicon Mac, throws. PPSA99203 only prints it (`[console] hw.model: %s`) - [Host CPU check](../../../core/libs/prx/libkernel/System/src/HostCpu.cpp) (libkernel) - when libkernel loads on a Linux or Windows x86-64 host, it logs which of the PS5 CPU's user-mode extensions CPUID reports and throws, naming the host CPU, if any extension is missing that LLVM selects from plain C/C++ for the PS5's Zen 2 (`-march=znver2`, [feature list](https://github.com/llvm/llvm-project/blob/llvmorg-19.1.7/llvm/lib/Target/X86/X86.td#L1459-L1513)): SSE to SSE4.2, SSSE3, POPCNT, LZCNT, BMI1, BMI2, MOVBE, CMPXCHG16B, LAHF/SAHF, AVX, AVX2, FMA, F16C, and the operating system's AVX state in XCR0 (#3719). SSE4A, CLZERO and SHA, which `--to-intel` converts, and AES, PCLMULQDQ, RDRAND, RDSEED, ADX, CLFLUSHOPT, CLWB, RDPID, XSAVEOPT, XSAVEC and PREFETCHW, which compilers emit only for intrinsics or explicit prefetches, are logged but not required: a title that executes one of them on a host without it still stops with an illegal instruction. macOS hosts are not checked: some Rosetta versions execute extensions without reporting them through CPUID, but support differs by host. ANDN and RDRAND fault on an M1 with macOS 15.3.1; see the [Rosetta conversion limits](relinker.md) From 2026238d3912150cd0a345e19b8c86467e6a0cb1 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 21 Oct 2026 06:53:45 -0400 Subject: [PATCH 262/318] test(libkernel): expect ENOENT for unresolved sysctl names --- core/libs/tests/GuestSystemConfiguration.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/core/libs/tests/GuestSystemConfiguration.cpp b/core/libs/tests/GuestSystemConfiguration.cpp index 02140768bd..a4d441e2e0 100644 --- a/core/libs/tests/GuestSystemConfiguration.cpp +++ b/core/libs/tests/GuestSystemConfiguration.cpp @@ -793,16 +793,15 @@ static void CheckNameToMib() { char tooLong[1025]; std::memset(tooLong, 'a', sizeof(tooLong) - 1); tooLong[sizeof(tooLong) - 1] = 0; - Require(sysctlnametomib_nid_postfix(tooLong, mib, &size) == -1 && *__error_nid_postfix() == 63); + int tooLongMib[4] = {0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a, 0x5a5a5a5a}; + std::size_t tooLongMibSize = 4; + Require(sysctlnametomib_nid_postfix(tooLong, tooLongMib, &tooLongMibSize) == -1 && *__error_nid_postfix() == 63); + Require(tooLongMibSize == 4 && tooLongMib[0] == 0x5a5a5a5a && tooLongMib[1] == 0x5a5a5a5a && tooLongMib[2] == 0x5a5a5a5a && tooLongMib[3] == 0x5a5a5a5a); tooLong[1023] = 0; + *__error_nid_postfix() = 13; + Require(sysctlnametomib_nid_postfix(tooLong, tooLongMib, &tooLongMibSize) == -1 && *__error_nid_postfix() == 2 && tooLongMibSize == 4); + Require(tooLongMib[0] == 0x5a5a5a5a && tooLongMib[1] == 0x5a5a5a5a && tooLongMib[2] == 0x5a5a5a5a && tooLongMib[3] == 0x5a5a5a5a); bool threw = false; - try { - sysctlnametomib_nid_postfix(tooLong, mib, &size); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw); - threw = false; try { sysctlnametomib_nid_postfix(nullptr, mib, &size); } catch (const std::invalid_argument&) { From a7e441edc2c86444379fd1b2b2f63d9e6c1157a5 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:30:37 -0500 Subject: [PATCH 263/318] test(libSceUserService): cover initial user queries and game presets validation --- core/libs/tests/GuestUserService.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index db457f47f3..83e78239ca 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -7,6 +7,8 @@ #include extern "C" { +int APS5_VABI sceUserServiceGetInitialUser(int* user_id); +int APS5_VABI sceUserServiceGetGamePresets(int user_id, UserServiceGamePresets* presets); int APS5_VABI sceUserServiceGetUserNumber(int user_id, int32_t* number); int APS5_VABI sceUserServiceGetPlatformPrivacyWs1(int32_t user_id, int32_t* value); int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); @@ -154,6 +156,14 @@ int main() { Require(setting.get(USER_SERVICE_INITIAL_USER_ID, &value) == USER_SERVICE_OK); Require(value == setting.value); } + int initialUser = -1; + Require(sceUserServiceGetInitialUser(nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetInitialUser(&initialUser) == USER_SERVICE_OK && initialUser == USER_SERVICE_INITIAL_USER_ID); + Require(sceUserServiceGetGamePresets(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + UserServiceGamePresets presets{}; + presets.difficulty = 0x5a; + Require(sceUserServiceGetGamePresets(0, &presets) == USER_SERVICE_ERROR_INVALID_ARGUMENT && presets.difficulty == 0x5a); + Require(sceUserServiceGetGamePresets(USER_SERVICE_INITIAL_USER_ID, &presets) == USER_SERVICE_OK && presets.difficulty == 0); bool threwKeyremap = false; try { sceUserServiceGetAccessibilityKeyremapEnable(); From bdcf9c6e5dce59e4e58c97996ab97669626f21e6 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:31:05 -0500 Subject: [PATCH 264/318] test(libSceAvPlayer): cover null handle validation in sceAvPlayerAddSourceEx and sceAvPlayerDisableStream --- core/libs/tests/GuestAvPlayer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index 54220d0b27..b79a260a1c 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -39,6 +39,8 @@ int APS5_VABI sceAvPlayerChangeStream(AvPlayerInternal*, std::uint32_t, std::uin int APS5_VABI sceAvPlayerSetAvailableBandwidth(AvPlayerInternal*, std::uint32_t, std::uint32_t, std::uint32_t); int APS5_VABI sceAvPlayerStart(AvPlayerInternal*); int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal*, const AvPlayer::StartInfo*); +int APS5_VABI sceAvPlayerAddSourceEx(AvPlayerInternal*, std::uint32_t, const void*); +int APS5_VABI sceAvPlayerDisableStream(AvPlayerInternal*, std::uint32_t); int APS5_VABI sceAvPlayerStop(AvPlayerInternal*); int APS5_VABI sceAvPlayerPause(AvPlayerInternal*); int APS5_VABI sceAvPlayerResume(AvPlayerInternal*); @@ -1090,6 +1092,8 @@ void TestExtendedExports() { Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no frame after extended restart"); Check(frame.timestamp <= 67, "zero-position start did not restart playback"); Check(sceAvPlayerClose(player) == 0, "close failed"); + Check(sceAvPlayerAddSourceEx(nullptr, 0, nullptr) == InvalidParams, "null player add source rejected"); + Check(sceAvPlayerDisableStream(nullptr, 0) == InvalidParams, "null player disable stream rejected"); } void TestWithoutAllocators() { From 9d15da71a6727f6b850e445ae33373bfab0ee7ab Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:31:43 -0500 Subject: [PATCH 265/318] test(libSceNpEntitlementAccess): cover flag queries and transaction parameter validation --- core/libs/tests/GuestEntitlements.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/GuestEntitlements.cpp b/core/libs/tests/GuestEntitlements.cpp index 868c1f6e7f..46da7dd6b4 100644 --- a/core/libs/tests/GuestEntitlements.cpp +++ b/core/libs/tests/GuestEntitlements.cpp @@ -14,6 +14,10 @@ extern "C" { int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfo(std::uint32_t, const NpUnifiedEntitlementLabel*, NpEntitlementAccessAddcontEntitlementInfo*); int APS5_VABI sceNpEntitlementAccessGetAddcontEntitlementInfoList(std::uint32_t, NpEntitlementAccessAddcontEntitlementInfo*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceNpEntitlementAccessGetSkuFlag(std::uint32_t*); +int APS5_VABI sceNpEntitlementAccessGetPftFlag(std::uint32_t*); +int APS5_VABI sceNpEntitlementAccessGetGameTrialsFlag(std::uint32_t*); +int APS5_VABI sceNpEntitlementAccessGenerateTransactionId(void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -31,7 +35,17 @@ int main(int argc, char** argv) { const auto directory = root / std::filesystem::path(u8"dlc-a\u00e7\u00e3o-\u65e5\u672c"); const auto file = directory / "anyps5-entitlements.ini"; if (argc == 2 && std::string_view(argv[1]) == "missing") { - SetEntitlements(file); + std::uint32_t skuFlag = 0; + Require(sceNpEntitlementAccessGetSkuFlag(nullptr) == static_cast(0x80558003)); + Require(sceNpEntitlementAccessGetSkuFlag(&skuFlag) == 0 && skuFlag == 3); + std::uint32_t pftFlag = 1; + Require(sceNpEntitlementAccessGetPftFlag(nullptr) == static_cast(0x817D0002)); + Require(sceNpEntitlementAccessGetPftFlag(&pftFlag) == 0 && pftFlag == 0); + std::uint32_t trialFlag = 1; + Require(sceNpEntitlementAccessGetGameTrialsFlag(nullptr) == static_cast(0x817D0002)); + Require(sceNpEntitlementAccessGetGameTrialsFlag(&trialFlag) == 0 && trialFlag == 0); + Require(sceNpEntitlementAccessGenerateTransactionId(nullptr) == static_cast(0x80558003)); + SetEntitlements(file); std::uint32_t hits = 0; bool rejected = false; try { From 94515fd50604ba3b30fc7640c96007d5ed2a9b77 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:32:09 -0500 Subject: [PATCH 266/318] test(libSceWebBrowserDialog): cover uninitialized rejection for cookie operations --- core/libs/tests/GuestWebBrowserDialog.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp index bb391077a8..6289900e72 100644 --- a/core/libs/tests/GuestWebBrowserDialog.cpp +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -6,6 +6,8 @@ extern "C" { int APS5_VABI sceWebBrowserDialogInitialize(void); int APS5_VABI sceWebBrowserDialogTerminate(void); +int APS5_VABI sceWebBrowserDialogSetCookie(const void* param); +int APS5_VABI sceWebBrowserDialogResetCookie(const void* param); int APS5_VABI sceWebBrowserDialogOpen(const void* param); int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent(const void* param, const WebBrowserDialogAllowList* allowList); int APS5_VABI sceWebBrowserDialogClose(void); @@ -37,6 +39,8 @@ int main() { result.result = 0x5a5a5a5a; Require(sceWebBrowserDialogTerminate() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogSetCookie(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); + Require(sceWebBrowserDialogResetCookie(nullptr) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); Require(sceWebBrowserDialogClose() == COMMON_DIALOG_ERROR_NOT_INITIALIZED); Require(sceWebBrowserDialogOpen(param) == COMMON_DIALOG_ERROR_NOT_INITIALIZED); From 8713da0cbb352474529b29a483b338992e8edaa2 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:33:11 -0500 Subject: [PATCH 267/318] test(libSceSystemGesture): cover debug version query --- core/libs/tests/GuestSystemGesture.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestSystemGesture.cpp b/core/libs/tests/GuestSystemGesture.cpp index 656f943aca..c1e62e8934 100644 --- a/core/libs/tests/GuestSystemGesture.cpp +++ b/core/libs/tests/GuestSystemGesture.cpp @@ -31,6 +31,7 @@ int APS5_VABI sceSystemGestureUpdatePrimitiveTouchRecognizer(std::int32_t, const int APS5_VABI sceSystemGestureInitializePrimitiveTouchRecognizer(); int APS5_VABI sceSystemGestureFinalizePrimitiveTouchRecognizer(void); int APS5_VABI sceSystemGestureGetPrimitiveTouchEventByPrimitiveID(std::int32_t, std::uint16_t, void*); +int APS5_VABI sceSystemGestureDebugGetVersion(void); } static void Require(bool value) { if (!value) std::abort(); } @@ -280,6 +281,7 @@ int main() { Require(sceSystemGestureUpdateAllTouchRecognizer(handle + 1) == invalidHandle); Require(sceSystemGestureUpdatePrimitiveTouchRecognizer(handle + 1, nullptr) == invalidArgument); + Require(sceSystemGestureDebugGetVersion() == 0x6a00); TestTouchRecognizerInformation(handle, invalidArgument, invalidHandle); TestPrimitiveTouchEvents(handle, invalidArgument, invalidHandle, indexOutOfArray); std::array otherHandles{}; From db8785f6ced6b240e4005f3d4a0978e5bdd54ac8 Mon Sep 17 00:00:00 2001 From: Project-MOC <83237812+Project-MOC@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:52:02 +0200 Subject: [PATCH 268/318] fix(tools): preserve new SELF output permissions --- tools/tests/test_unwrap_self.py | 93 +++++++++++++++++++++++++++++++-- tools/unwrap_self.py | 11 ++-- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/tools/tests/test_unwrap_self.py b/tools/tests/test_unwrap_self.py index 648d5471b2..c45b80fe40 100644 --- a/tools/tests/test_unwrap_self.py +++ b/tools/tests/test_unwrap_self.py @@ -127,7 +127,7 @@ class UnwrapTest(unittest.TestCase): def failing_open(path, mode="r", *args, **kwargs): stream = real_open(path, mode, *args, **kwargs) - if "w" not in mode: + if "w" not in mode and "x" not in mode: return stream writer = mock.MagicMock(wraps=stream) writer.__enter__.return_value = writer @@ -199,7 +199,7 @@ class UnwrapTest(unittest.TestCase): def failing_open(path, mode="r", *args, **kwargs): stream = real_open(path, mode, *args, **kwargs) - if "w" not in mode: + if "w" not in mode and "x" not in mode: return stream writer = mock.MagicMock(wraps=stream) writer.__enter__.return_value = writer @@ -227,14 +227,99 @@ class UnwrapTest(unittest.TestCase): output.write_bytes(b"previous output") before = set(directory.iterdir()) errors = io.StringIO() - with mock.patch.object(tempfile, "NamedTemporaryFile", side_effect=OSError("simulated creation failure")), \ - contextlib.redirect_stderr(errors): + real_open = io.open + + def failing_open(path, mode="r", *args, **kwargs): + if "x" in mode: + raise OSError("simulated creation failure") + return real_open(path, mode, *args, **kwargs) + + with mock.patch("io.open", side_effect=failing_open), contextlib.redirect_stderr(errors): status = unwrap_self.main([str(source), str(output)]) self.assertEqual(status, 2) self.assertIn("simulated creation failure", errors.getvalue()) self.assertEqual(output.read_bytes(), b"previous output") self.assertEqual(set(directory.iterdir()), before) + def test_temporary_name_collision_preserves_all_existing_files(self): + directory = Path(self.directory) + source = directory / "input.self" + output = directory / "output.elf" + collision = directory / ".unwrap-self-collision" + container = build_container(default_segments(), default_records(), default_payloads()) + source.write_bytes(container) + output.write_bytes(b"previous output") + collision.write_bytes(b"unrelated file") + before = set(directory.iterdir()) + errors = io.StringIO() + with mock.patch.object(unwrap_self.secrets, "token_hex", return_value="collision"), \ + contextlib.redirect_stderr(errors): + status = unwrap_self.main([str(source), str(output)]) + self.assertEqual(status, 2) + self.assertEqual(output.read_bytes(), b"previous output") + self.assertEqual(collision.read_bytes(), b"unrelated file") + self.assertEqual(source.read_bytes(), container) + self.assertEqual(set(directory.iterdir()), before) + + @unittest.skipUnless(sys.platform == "win32", "Windows ACL inheritance") + def test_new_output_inherits_the_destination_directory_acl(self): + directory = Path(self.directory) + grant = subprocess.run(["icacls.exe", str(directory), "/grant", "*S-1-1-0:(OI)(CI)(RX)"], + capture_output=True, text=True, timeout=30) + self.assertEqual(grant.returncode, 0, grant.stdout + grant.stderr) + container = build_container(default_segments(), default_records(), default_payloads()) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + permissions = directory / "output-acl.txt" + saved = subprocess.run(["icacls.exe", str(output), "/save", str(permissions)], + capture_output=True, text=True, timeout=30) + self.assertEqual(saved.returncode, 0, saved.stdout + saved.stderr) + self.assertIn(";;;WD)", permissions.read_text(encoding="utf-16-le")) + self.assertTrue(output.read_bytes().startswith(b"\x7fELF")) + self.assertEqual((directory / "input.self").read_bytes(), container) + self.assertFalse(any(path.name.startswith(".unwrap-self-") for path in directory.iterdir())) + + @unittest.skipUnless(os.name == "posix", "POSIX permission bits") + def test_new_output_permissions_follow_the_process_umask(self): + directory = Path(self.directory) + source = directory / "input.self" + container = build_container(default_segments(), default_records(), default_payloads()) + source.write_bytes(container) + for mask in (0o000, 0o022, 0o027, 0o077, 0o200, 0o777): + with self.subTest(umask=oct(mask)): + output = directory / f"output-{mask:o}.elf" + result = subprocess.run([sys.executable, str(Path(unwrap_self.__file__)), str(source), str(output)], + capture_output=True, text=True, timeout=30, umask=mask) + self.assertEqual(result.returncode, 0, result.stderr) + mode = output.stat().st_mode & 0o777 + self.assertEqual(mode, 0o666 & ~mask) + output.chmod(mode | 0o400) + self.assertTrue(output.read_bytes().startswith(b"\x7fELF")) + self.assertFalse(any(path.name.startswith(".unwrap-self-") for path in directory.iterdir())) + self.assertEqual(source.read_bytes(), container) + + @unittest.skipUnless(os.name == "posix", "POSIX group inheritance") + def test_new_output_inherits_the_destination_directory_group(self): + groups = [group for group in os.getgroups() if group != os.getegid()] + if not groups: + self.skipTest("requires a supplementary group") + directory = Path(self.directory) + group = groups[0] + try: + os.chown(directory, -1, group) + directory.chmod(0o2700) + except PermissionError as error: + self.skipTest(str(error)) + if not directory.stat().st_mode & 0o2000: + self.skipTest("filesystem does not retain the setgid bit") + container = build_container(default_segments(), default_records(), default_payloads()) + result, output = self.convert(container) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(output.stat().st_gid, group) + self.assertTrue(output.read_bytes().startswith(b"\x7fELF")) + self.assertEqual((directory / "input.self").read_bytes(), container) + self.assertEqual(set(directory.iterdir()), {directory / "input.self", output}) + @unittest.skipUnless(os.name == "posix", "POSIX permission bits") def test_replacing_output_preserves_existing_permissions(self): directory = Path(self.directory) diff --git a/tools/unwrap_self.py b/tools/unwrap_self.py index 26f7bfd320..f77a000caf 100644 --- a/tools/unwrap_self.py +++ b/tools/unwrap_self.py @@ -31,9 +31,9 @@ usage: unwrap_self.py [--verbose] import argparse import math +import secrets import struct import sys -import tempfile from collections import Counter from pathlib import Path @@ -328,17 +328,16 @@ def unwrap(path, output, verbose=False): % (item["index"], name, item["vaddr"], item["size"], item["new"], moved)) destination.parent.mkdir(parents=True, exist_ok=True) - temporary_path = None + temporary_path = destination.parent / (".unwrap-self-" + secrets.token_hex(16)) + temporary = temporary_path.open("xb") try: - with tempfile.NamedTemporaryFile(mode="wb", dir=destination.parent, prefix=".unwrap-self-", delete=False) as temporary: - temporary_path = Path(temporary.name) + with temporary: temporary.write(result) if sys.platform != "win32" and destination.exists(): temporary_path.chmod(destination.stat().st_mode) temporary_path.replace(destination) finally: - if temporary_path is not None: - temporary_path.unlink(missing_ok=True) + temporary_path.unlink(missing_ok=True) return len(result) From 5053c1e8eb97d345c857110d384ddda02747a8e6 Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:33:24 -0500 Subject: [PATCH 269/318] test(libSceAudio3d): cover speaker array parameter validation and memory query limits --- core/libs/tests/GuestAudio3dSpeakerArray.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestAudio3dSpeakerArray.cpp b/core/libs/tests/GuestAudio3dSpeakerArray.cpp index 7ef0c9942e..4f188b37c4 100644 --- a/core/libs/tests/GuestAudio3dSpeakerArray.cpp +++ b/core/libs/tests/GuestAudio3dSpeakerArray.cpp @@ -47,6 +47,7 @@ void RequireThrows(F f, const char* message) { int main() { Require(sceAudio3dGetSpeakerArrayMemorySize(4, 1) == sceAudioOut2GetSpeakerArrayMemorySize(4, 1, 0), "memory size forwards to AudioOut2 without ambisonics"); + RequireThrows([] { sceAudio3dGetSpeakerArrayMemorySize(0, 0); }, "zero speakers throws invalid_argument"); const AudioOut2Position positions[4] = {{1, 1, 0}, {1, -1, 0}, {-1, 1, 0}, {-1, -1, 0}}; alignas(16) unsigned char memory[1024] = {}; @@ -68,6 +69,7 @@ int main() { Require(sceAudio3dGetSpeakerArrayMixCoefficients2(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients2, 4, 1, 0.5f) == 0, "mix coefficients 2 succeed"); Require(coefficients2[3] == 0.0f, "mix coefficients 2 are written"); RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, coefficients, 3); }, "a coefficient count that differs from the array is rejected"); + RequireThrows([&] { sceAudio3dGetSpeakerArrayMixCoefficients(handle, AudioOut2Position{1, 0, 0}, 0.0f, nullptr, 4); }, "null coefficients pointer throws invalid_argument"); Require(sceAudio3dDeleteSpeakerArray(handle) == 0, "delete succeeds"); RequireThrows([&] { sceAudio3dDeleteSpeakerArray(handle); }, "a deleted array is no longer valid"); From 79e884902e289d6dd68045e5ae233d9108d7a90b Mon Sep 17 00:00:00 2001 From: al3w0f205 Date: Fri, 23 Oct 2026 11:33:49 -0500 Subject: [PATCH 270/318] test(libSceAudio3d): cover invalid reserved argument rejection in sceAudio3dInitialize --- core/libs/tests/GuestAudio3dParameters.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestAudio3dParameters.cpp b/core/libs/tests/GuestAudio3dParameters.cpp index 4335f1ff0f..dc4e19688a 100644 --- a/core/libs/tests/GuestAudio3dParameters.cpp +++ b/core/libs/tests/GuestAudio3dParameters.cpp @@ -6,6 +6,7 @@ #include extern "C" void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* parameters); +extern "C" int APS5_VABI sceAudio3dInitialize(std::int64_t reserved); static_assert(sizeof(Audio3dOpenParameters) == 0x28); static_assert(offsetof(Audio3dOpenParameters, num_beds) == 0x20); @@ -74,4 +75,5 @@ int main() { CheckInvalidDestination(); CheckShortBuffer(); CheckExtendedBuffer(); + Require(sceAudio3dInitialize(-1) == static_cast(0x80EA0004), "non-zero reserved parameter rejected in sceAudio3dInitialize"); } From ad6a55121ebb5285f65601fd37eb4fd87a0dc8a0 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Tue, 20 Oct 2026 11:57:56 +0000 Subject: [PATCH 271/318] fix(agc): read FMASK of up to 8 samples in every storage width Since the synthetic FMASK mode was removed, a direct FMASK load reads the stored word and only Fmask32_S8_F8 is accepted. A load from any other FMASK format of up to 8 samples now reads the 8-, 16- or 32-bit surface and expands it in the shader to 4 bits per sample, an index past the fragment count reading 8 (unknown). One runtime image mode handles every such format, reading the format from the descriptor at run time or from a specialization constant. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/GuestTextureResource.cpp | 9 +- .../tests/GuestTextureResource.cpp | 25 +++++- .../libSceAgcDriver/tests/ShaderMemory.cpp | 15 +++- .../tests/execution/FmaskImageReads.cpp | 88 ++++++++++++++++--- core/shader/recompiler/CMakeLists.txt | 1 + .../src/DescriptorBindingBuilder.cpp | 3 + .../Optimization/src/ResourceMaterializer.cpp | 11 ++- .../recompiler/PipelineSpecialization.hpp | 1 + .../RdnaDecoder/RdnaDescriptorFormat.hpp | 12 +++ .../RdnaDecoder/src/RdnaDescriptorFormat.cpp | 15 ++++ .../SpirvBackend/src/SpirvImageEmitter.cpp | 36 ++++++++ docs/dev/debt/shader.md | 2 +- 12 files changed, 198 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp index 1249e08037..866371295c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp @@ -179,12 +179,15 @@ GuestTextureResource DecodeTextureResource(std::span words) Require(baseLevel <= lastLevel, "guest texture descriptor has a base mip level past its last mip level"); const bool fmask = ShaderRecompiler::IsFmaskTextureFormat(static_cast(format)); + const auto fmaskLayout = ShaderRecompiler::FmaskLayoutOf(static_cast(format)); if (fmask) { - Require(format == static_cast(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "guest FMASK format is not implemented"); + Require(fmaskLayout.has_value(), "guest FMASK format is not implemented"); Require(!writeCompress && (!metaCompress || metaAddr != 0u), "FMASK CMASK compression is not implemented without readable metadata"); Require(!metaCompress || metaPipeAligned, "FMASK CMASK metadata must be pipe aligned"); + Require(!metaCompress || format == static_cast(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8), "FMASK CMASK compression is implemented only for 8-sample, 8-fragment FMASK"); Require((typeRaw == 9u || typeRaw == 13u) && tileModeRaw == 24u && baseLevel == 0u && lastLevel == 0u && maxMip == 0u, "FMASK reads require a single-mip 2D or 2D-array Z_64KB_X surface"); - for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented"); + const bool identity = format != static_cast(ShaderRecompiler::IrBufferFormat::Fmask32_S8_F8) && dstSelX == 4u && dstSelY == 5u && dstSelZ == 6u && dstSelW == 7u; + for (const auto selector : {dstSelX, dstSelY, dstSelZ, dstSelW}) Require(identity || selector == 0u || selector == 1u || selector == 4u, "FMASK destination channel selector is not implemented"); } const auto describe = [&] { return " (format " + std::to_string(format) + ", " + std::to_string(width) + "x" + std::to_string(height) + "x" + std::to_string(depth + 1) + ", type " + std::to_string(typeRaw) + ", tile " + std::to_string(tileModeRaw) + ", mips " + std::to_string(baseLevel) + ".." + std::to_string(lastLevel) + " of " + std::to_string(maxMip + 1) + ")"; }; @@ -265,7 +268,7 @@ GuestTextureResource DecodeTextureResource(std::span words) result.lastLevel = lastLevel; result.tileMode = tileMode; result.dimension = dimension; - result.format = fmask ? static_cast(ShaderRecompiler::IrBufferFormat::Format32UInt) : format; + result.format = !fmask ? format : static_cast(fmaskLayout->storageBits == 8u ? ShaderRecompiler::IrBufferFormat::Format8UInt : fmaskLayout->storageBits == 16u ? ShaderRecompiler::IrBufferFormat::Format16UInt : ShaderRecompiler::IrBufferFormat::Format32UInt); result.dstSelX = static_cast(dstSelX); result.dstSelY = static_cast(dstSelY); result.dstSelZ = static_cast(dstSelZ); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 0cc0c41ed7..ab7db93b7a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -184,8 +184,29 @@ void RunGuestTextureResourceTests() { fmaskArray.baseArray = 1u; const auto fmaskArrayResource = DecodeTextureResource(pack(fmaskArray)); Require(fmaskArrayResource.format == 20u && fmaskArrayResource.baseArray == 1u && fmaskArrayResource.depthOrLastArray == 2u, "FMASK normalization changed the array view"); - for (std::uint32_t format = 156u; format <= 168u; ++format) { - if (format == 166u) continue; + constexpr std::array, 8> narrowFormats{{{156u, 5u}, {157u, 5u}, {158u, 5u}, {159u, 5u}, {160u, 5u}, {161u, 5u}, {163u, 11u}, {165u, 20u}}}; + for (const auto& [format, surface] : narrowFormats) { + auto narrow = fmask; + narrow.format = format; + const auto narrowResource = DecodeTextureResource(pack(narrow)); + Require(narrowResource.format == surface && narrowResource.tileMode == TextureTileMode::kZ64KBX, "FMASK " + std::to_string(format) + " did not use an integer Z surface of its storage width"); + narrow.dstSelX = 4u; + narrow.dstSelY = 5u; + narrow.dstSelZ = 6u; + narrow.dstSelW = 7u; + Require(DecodeTextureResource(pack(narrow)).format == surface, "an expanded FMASK " + std::to_string(format) + " view with identity selectors was refused"); + auto compressedNarrow = narrow; + compressedNarrow.metaCompress = compressedNarrow.metaPipeAligned = true; + compressedNarrow.metaAddr = 0x123456789aull; + rejectFields(compressedNarrow, "FMASK CMASK compression is implemented only for 8-sample, 8-fragment FMASK"); + } + auto identityWord = fmask; + identityWord.dstSelX = 4u; + identityWord.dstSelY = 5u; + identityWord.dstSelZ = 6u; + identityWord.dstSelW = 7u; + rejectFields(identityWord, "FMASK destination channel selector is not implemented"); + for (const auto format : {162u, 164u, 167u, 168u}) { auto unsupported = fmask; unsupported.format = format; rejectFields(unsupported, "FMASK format is not implemented"); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index effc0dc179..63fcc75d84 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -7577,8 +7577,19 @@ void verifyFmaskImageMode() { const auto modes = ResourceMaterializer::RuntimeImageModes(image); const auto selected = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); require(modes[selected].numericClass == IrTextureNumericClass::Uint && modes[selected].packedFormat == IrBufferFormat::Invalid && !modes[selected].constantSwizzle, "FMASK did not select a bound integer image mode"); - for (std::uint32_t format = 156u; format <= 168u; ++format) { - if (format == 166u) continue; + for (const auto format : {156u, 157u, 158u, 159u, 160u, 161u, 163u, 165u}) { + descriptor.dwords[1] = format << 20u; + const auto expanded = ResourceMaterializer::RuntimeImageMode(image, descriptor, modes); + require(expanded != selected && modes[expanded].numericClass == IrTextureNumericClass::Uint && modes[expanded].conversionFormat == FmaskExpansion && modes[expanded].dimension == RdnaImageDimension::Dim2D, "an FMASK format of up to 8 samples did not select the FMASK expansion mode"); + } + require(std::ranges::count_if(modes, [](const ImageResource& mode) { return mode.conversionFormat == FmaskExpansion; }) == 1, "FMASK expansion needs exactly one runtime image mode"); + for (const auto format : {161u, 166u}) { + descriptor.dwords[1] = format << 20u; + descriptor.dwords[3] = 0x91800005u; + expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "FMASK destination channel selector is not implemented", "an FMASK load selecting Y was accepted"); + } + descriptor.dwords[3] = 0x91800004u; + for (const auto format : {162u, 164u, 167u, 168u}) { descriptor.dwords[1] = format << 20u; expectFailure([&] { static_cast(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "FMASK format is not implemented", "an unsupported FMASK format was accepted"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp index b76ffe2a6d..e4b54fc027 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/FmaskImageReads.cpp @@ -32,9 +32,9 @@ std::array Buffer(const void* data, std::uint32_t bytes) { return {static_cast(address), static_cast(address >> 32u) & 0xffffu, bytes, 0x31016facu}; } -std::array Image(bool layered, std::uint32_t pipeXor, std::uint32_t swizzle = 4u) { +std::array Image(bool layered, std::uint32_t pipeXor, std::uint32_t swizzle = 4u, std::uint32_t format = 166u) { const auto address = reinterpret_cast(Texels.data()) + pipeXor; - return {static_cast(address >> 8u), static_cast(address >> 40u) | (166u << 20u) | (3u << 30u), 31u | ((Height - 1u) << 14u), swizzle | (24u << 20u) | ((layered ? 13u : 9u) << 28u), layered ? 2u | (1u << 16u) : 0u, 0u, 0u, 0u}; + return {static_cast(address >> 8u), static_cast(address >> 40u) | (format << 20u) | (3u << 30u), 31u | ((Height - 1u) << 14u), swizzle | (24u << 20u) | ((layered ? 13u : 9u) << 28u), layered ? 2u | (1u << 16u) : 0u, 0u, 0u, 0u}; } std::uint32_t Mapping(std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t round) { @@ -56,15 +56,16 @@ std::uint64_t EquationOffset(const TextureSwizzleEquation& equation, std::uint32 return offset; } -void Fill(bool layered, std::uint32_t pipeXor, std::uint32_t round) { +template +void FillWith(bool layered, std::uint32_t pipeXor, std::uint32_t surfaceFormat, std::uint32_t bytes, TValue valueAt) { auto words = Image(layered, pipeXor); - words[1] = (words[1] & ~(0x1ffu << 20u)) | (20u << 20u); + words[1] = (words[1] & ~(0x1ffu << 20u)) | (surfaceFormat << 20u); const auto resource = DecodeTextureResource(words); const auto geometry = DescribeSurface(resource); Require(geometry.guestBytes <= Texels.size(), "FMASK fixture exceeds its allocation"); const auto& mip = geometry.mips.at(0); - const auto block = ThinBlockLayout(resource.tileMode, 4u); - const auto* equation = FindTextureSwizzleEquation(24u, 4u); + const auto block = ThinBlockLayout(resource.tileMode, bytes); + const auto* equation = FindTextureSwizzleEquation(24u, bytes); Require(equation != nullptr, "FMASK fixture has no Z swizzle equation"); std::ranges::fill(Texels, std::byte{0}); for (std::uint32_t layer = 0; layer < geometry.layers; ++layer) { @@ -73,14 +74,18 @@ void Fill(bool layered, std::uint32_t pipeXor, std::uint32_t round) { auto offset = geometry.GuestLayerOffset(layer) + mip.tiledOffset; if (mip.tail) offset += EquationOffset(*equation, x + mip.tailX, y + mip.tailY, layer) ^ pipeXor; else offset += (static_cast(y / block[2]) * mip.blocksPerRow + x / block[1]) * block[0] + (EquationOffset(*equation, x, y, layer) ^ pipeXor); - const auto value = Mapping(x, y, layer, round); - Require(offset + sizeof(value) <= Texels.size(), "FMASK fixture addresses outside its allocation"); - std::memcpy(Texels.data() + offset, &value, sizeof(value)); + const std::uint32_t value = valueAt(x, y, layer); + Require(offset + bytes <= Texels.size(), "FMASK fixture addresses outside its allocation"); + std::memcpy(Texels.data() + offset, &value, bytes); } } } } +void Fill(bool layered, std::uint32_t pipeXor, std::uint32_t round) { + FillWith(layered, pipeXor, 20u, 4u, [round](std::uint32_t x, std::uint32_t y, std::uint32_t layer) { return Mapping(x, y, layer, round); }); +} + ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::span image, bool registration) { std::vector userData(16u); const auto input = Buffer(Input.data(), sizeof(Input)); @@ -140,6 +145,68 @@ void Run(AgcDriver::VulkanDevice& device, bool layered, bool registration, std:: } } +struct NarrowFmask { + std::uint32_t format; + std::uint32_t surfaceFormat; + std::uint32_t bytes; + std::uint32_t samples; + std::uint32_t fragments; + std::uint32_t sampleBits; + std::uint32_t identity; + std::uint32_t expandedIdentity; +}; + +constexpr std::array NarrowFormats{{ + {156u, 5u, 1u, 2u, 1u, 1u, 0x02u, 0x80u}, + {157u, 5u, 1u, 4u, 1u, 1u, 0x0eu, 0x8880u}, + {158u, 5u, 1u, 8u, 1u, 1u, 0xfeu, 0x88888880u}, + {159u, 5u, 1u, 2u, 2u, 1u, 0x02u, 0x10u}, + {160u, 5u, 1u, 4u, 2u, 2u, 0xa4u, 0x8810u}, + {161u, 5u, 1u, 4u, 4u, 2u, 0xe4u, 0x3210u}, + {163u, 11u, 2u, 8u, 2u, 2u, 0xaaa4u, 0x88888810u}, + {165u, 20u, 4u, 8u, 4u, 4u, 0x44443210u, 0x88883210u}, +}}; + +std::uint32_t Stored(const NarrowFmask& fmask, std::uint32_t x, std::uint32_t y, std::uint32_t layer, std::uint32_t round) { + if (round == 0u) return fmask.identity; + const auto value = (x * 0x9e3779b1u) ^ (y * 0x85ebca77u) ^ (layer * 0xc2b2ae3du) ^ (fmask.format * 0x27d4eb2fu); + return fmask.bytes == 4u ? value : value & ((1u << (fmask.bytes * 8u)) - 1u); +} + +std::uint32_t Expanded(const NarrowFmask& fmask, std::uint32_t stored) { + std::uint32_t expanded = 0; + for (std::uint32_t sample = 0; sample < fmask.samples; ++sample) { + const auto fragment = (stored >> (sample * fmask.sampleBits)) & ((1u << fmask.sampleBits) - 1u); + expanded |= (fragment >= fmask.fragments ? 8u : fragment) << (sample * 4u); + } + return expanded; +} + +void RunNarrow(AgcDriver::VulkanDevice& device, const NarrowFmask& fmask, bool layered, bool registration) { + auto code = Code; + if (layered) code[4] = 0xf0001f28u; + Require(Expanded(fmask, fmask.identity) == fmask.expandedIdentity, "FMASK " + std::to_string(fmask.format) + " reference does not expand PAL's identity value"); + for (std::uint32_t round = 0; round < 2u; ++round) { + FillWith(layered, 0u, fmask.surfaceFormat, fmask.bytes, [&](std::uint32_t x, std::uint32_t y, std::uint32_t layer) { return Stored(fmask, x, y, layer, round); }); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Input[lane * 4u] = (lane * 37u + 11u) % Width; + Input[lane * 4u + 1u] = (lane * 23u + 5u) % Height; + Input[lane * 4u + 2u] = layered ? lane & 1u : 0u; + Input[lane * 4u + 3u] = 0u; + } + Output.fill(Sentinel); + const auto shader = Compile(device, code, Image(layered, 0u, 4u, fmask.format), registration); + device.Dispatch(shader, 1u, 1u, 1u); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + const auto stored = Stored(fmask, Input[lane * 4u], Input[lane * 4u + 1u], layered ? Input[lane * 4u + 2u] + 1u : 0u, round); + const std::array expected{Expanded(fmask, stored), 0u, 0u, 0u}; + for (std::uint32_t component = 0; component < 4u; ++component) Require(Output[lane * 4u + component] == expected[component], "FMASK " + std::to_string(fmask.format) + " load did not expand its mapping: round " + std::to_string(round) + ", lane " + std::to_string(lane) + ", component " + std::to_string(component) + ", read " + std::to_string(Output[lane * 4u + component]) + ", expected " + std::to_string(expected[component])); + } + Require(std::all_of(Output.begin() + Threads * 4u, Output.end(), [](auto value) { return value == Sentinel; }), "FMASK load wrote outside its output"); + } +} + } int main() { @@ -149,9 +216,10 @@ int main() { for (const bool registration : {false, true}) { for (const bool layered : {false, true}) { for (const auto pipeXor : {0u, 0xf00u}) Run(*device, layered, registration, pipeXor); + for (const auto& fmask : NarrowFormats) RunNarrow(*device, fmask, layered, registration); } } - std::cout << "FMASK image reads passed: 40 dispatches, 5120 component checks, 8 compression refusals\n"; + std::cout << "FMASK image reads passed: 104 dispatches, 13312 component checks, 8 compression refusals\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 0f83ea58a8..07dee3a0d9 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -249,6 +249,7 @@ function(add_shader_recompiler target runtimeTarget) ImageTableAbi::PoisonCode ImageTableAbi::PoisonReasonName ImageTableAbi::TableHeader + FmaskLayoutOf IsFmaskTextureFormat PipelineSpecialization::IsWorkgroupOffset PipelineSpecialization::WorkgroupOffsetBase diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 170b97d6b8..51b8c966e9 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -1,5 +1,6 @@ #include "Optimization/DescriptorBindingBuilder.hpp" #include "Optimization/ResourceMaterializer.hpp" +#include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvBufferFormat.hpp" #include @@ -355,6 +356,8 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l } for (std::uint32_t index = 0; index < info.images.size() && !registrationModules; ++index) { if (info.images[index].table != NoTable) continue; + const auto& modes = info.runtimeImageModes.at(index); + if (std::ranges::any_of(modes, [](const ImageResource& mode) { return mode.conversionFormat == FmaskExpansion; })) plan.specialization.push_back({PipelineSpecialization::FmaskFormatBase + index, modes.at(imageModes[index]).conversionFormat == FmaskExpansion ? (snapshot.images.at(index).dwords[1] >> 20u) & 0x1ffu : 0u}); const auto first = PipelineSpecialization::ImageBase + index * PipelineSpecialization::ImageWords; plan.specialization.push_back({first, imageModes[index]}); for (std::uint32_t component = 0; component < 4u; ++component) plan.specialization.push_back({first + 1u + component, (snapshot.images.at(index).dwords[3] >> (component * 3u)) & 7u}); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 5478136a1a..6514f474aa 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -232,9 +232,14 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag if (decoded.fmask && (storage || base.depthCompare || table)) { return rejectImage(PoisonReason::Fmask, "FMASK requires a direct sampled image load"); } - if (decoded.fmask && format != IrBufferFormat::Fmask32_S8_F8) return rejectImage(PoisonReason::Fmask, "FMASK format is not implemented"); + if (decoded.fmask && !FmaskLayoutOf(format).has_value()) return rejectImage(PoisonReason::Fmask, "FMASK format is not implemented"); if (decoded.fmask && ((descriptor.dwords[6] & (1u << 20u)) != 0u || ((descriptor.dwords[6] & (1u << 21u)) != 0u && (descriptor.dwords[6] >> 24u) == 0u && descriptor.dwords[7] == 0u))) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is not implemented without readable metadata"); + if (decoded.fmask && (descriptor.dwords[6] & (1u << 21u)) != 0u && format != IrBufferFormat::Fmask32_S8_F8) return rejectImage(PoisonReason::Fmask, "FMASK CMASK compression is implemented only for 8-sample, 8-fragment FMASK"); if (decoded.fmask && (base.packed || base.byElements != 0u)) return rejectImage(PoisonReason::Fmask, "packed FMASK image reads are not implemented"); + for (std::uint32_t component = 0; decoded.fmask && component < 4u; ++component) { + const auto selector = (descriptorImageSwizzle(descriptor) >> (component * 3u)) & 7u; + if (selector != 0u && selector != 1u && selector != 4u) return rejectImage(PoisonReason::Fmask, "FMASK destination channel selector is not implemented"); + } if (base.packed) { if (table || (!storage && descriptorImageSwizzle(descriptor) != ShaderImageIdentitySwizzle)) { return rejectImage(PoisonReason::InvalidFormat, "packed image access requires a direct image, with identity swizzle when sampled"); @@ -271,6 +276,7 @@ ImageDecode inspectImageDescriptor(const DescriptorValue& descriptor, const Imag if (((descriptorImageSwizzle(descriptor) >> (component * 3u)) & 7u) == 7u) return rejectImage(PoisonReason::Conversion, "selects a channel the converted image format does not have"); } } + if (decoded.fmask && format != IrBufferFormat::Fmask32_S8_F8) decoded.conversionFormat = FmaskExpansion; decoded.srgbDecode = !storage && !base.depthCompare && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; if (decoded.srgbDecode && !base.srgbDecodeCompatible) { return rejectImage(PoisonReason::SrgbUnsupported, "samples or gathers an sRGB image the device cannot sample, which is not implemented"); @@ -1882,7 +1888,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe mode.shaderSwizzle = ShaderImageIdentitySwizzle; if (conversion == IrBufferFormat::Format11_11_10UNorm || conversion == IrBufferFormat::Format10_11_11Float) mode.shaderSwizzle = 0x2acu; modes.push_back(mode); - const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible; + const bool volumeLoad = (image.dimension == RdnaImageDimension::Dim2D || image.dimension == RdnaImageDimension::Dim2DArray) && image.fmaskCompatible && conversion != FmaskExpansion; const bool volumeStore = image.dimension == RdnaImageDimension::Dim2DArray && storage && image.written && !image.read && !image.atomic && image.mipMode != ImageMipMode::DynamicStorage; const bool volumePlaneBias = image.dimension == RdnaImageDimension::Dim2D && image.singleMipPlaneBiasCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; const bool volumeArraySample = image.dimension == RdnaImageDimension::Dim2DArray && image.volumeArraySampleCompatible && !storage && image.table == NoTable && conversion == IrBufferFormat::Invalid; @@ -1982,6 +1988,7 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe append(IrTextureNumericClass::Float, IrBufferFormat::Invalid, IrBufferFormat::Invalid, true, false); append(IrTextureNumericClass::Float, IrBufferFormat::Invalid, IrBufferFormat::Invalid, true, true); } + if (image.fmaskCompatible && !table) append(IrTextureNumericClass::Uint, FmaskExpansion, IrBufferFormat::Invalid, false, false); } } } diff --git a/core/shader/recompiler/PipelineSpecialization.hpp b/core/shader/recompiler/PipelineSpecialization.hpp index ae9ec178cf..2adcd7e04a 100644 --- a/core/shader/recompiler/PipelineSpecialization.hpp +++ b/core/shader/recompiler/PipelineSpecialization.hpp @@ -42,6 +42,7 @@ inline constexpr std::uint32_t DescriptorIndexBase = 16384u; inline constexpr std::uint32_t DescriptorIndexStride = 128u; inline constexpr std::uint32_t FoldOffsetBase = 57344u; inline constexpr std::uint32_t UnnormalizedBaseLevelBase = 57408u; +inline constexpr std::uint32_t FmaskFormatBase = 57472u; inline constexpr std::uint32_t DescriptorIndex(std::uint32_t binding, std::uint32_t element) { return DescriptorIndexBase + binding * DescriptorIndexStride + element; diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index e36217d958..83c1f217aa 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -4,6 +4,7 @@ #include "IntermediateRepresentation/IrMetadata.hpp" #include #include +#include #include namespace ShaderRecompiler { @@ -21,6 +22,17 @@ enum class ImageType : std::uint32_t { [[nodiscard]] IrTextureNumericClass VertexInputNumericClass(IrBufferFormat format); [[nodiscard]] bool IsFmaskTextureFormat(IrBufferFormat format); + +struct FmaskLayout { + std::uint32_t samples; + std::uint32_t fragments; + std::uint32_t sampleBits; + std::uint32_t storageBits; +}; + +inline constexpr IrBufferFormat FmaskExpansion = IrBufferFormat::Fmask8_S2_F1; + +[[nodiscard]] std::optional FmaskLayoutOf(IrBufferFormat format); [[nodiscard]] IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format); [[nodiscard]] IrBufferFormat RemapTextureFormat(IrBufferFormat format); [[nodiscard]] IrBufferFormat RemapStorageTextureFormat(IrBufferFormat format); diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index 763deb31fb..acdbad34d0 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -112,6 +112,21 @@ bool IsFmaskTextureFormat(IrBufferFormat format) { return format >= IrBufferFormat::Fmask8_S2_F1 && format <= IrBufferFormat::Fmask64_S16_F8; } +std::optional FmaskLayoutOf(IrBufferFormat format) { + switch (format) { + case IrBufferFormat::Fmask8_S2_F1: return FmaskLayout{2u, 1u, 1u, 8u}; + case IrBufferFormat::Fmask8_S4_F1: return FmaskLayout{4u, 1u, 1u, 8u}; + case IrBufferFormat::Fmask8_S8_F1: return FmaskLayout{8u, 1u, 1u, 8u}; + case IrBufferFormat::Fmask8_S2_F2: return FmaskLayout{2u, 2u, 1u, 8u}; + case IrBufferFormat::Fmask8_S4_F2: return FmaskLayout{4u, 2u, 2u, 8u}; + case IrBufferFormat::Fmask8_S4_F4: return FmaskLayout{4u, 4u, 2u, 8u}; + case IrBufferFormat::Fmask16_S8_F2: return FmaskLayout{8u, 2u, 2u, 16u}; + case IrBufferFormat::Fmask32_S8_F4: return FmaskLayout{8u, 4u, 4u, 32u}; + case IrBufferFormat::Fmask32_S8_F8: return FmaskLayout{8u, 8u, 4u, 32u}; + default: return std::nullopt; + } +} + IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format) { const FormatInfo* info = FindFormatInfo(format); if (info == nullptr || !info->sampled) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 804aeda284..6b4c1b2d0d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -558,11 +558,47 @@ void RequireConvertedUnormAccess(SpirvValueEmitContext& ctx, const ImageEmitAcce } } +std::uint32_t ExpandFmaskTexel(SpirvEmitterState& state, const ImageEmitAccess& access, std::uint32_t texel) { + const auto resource = access.mem.resource; + const auto u32 = TypeU32(state); + const auto format = state.registrationModules + ? Binary(state, spv::OpBitwiseAnd, u32, Binary(state, spv::OpShiftRightLogical, u32, RuntimeImageDword(state, resource, offsetof(RuntimeAbi::ResourceMetadata, descriptor) / sizeof(std::uint32_t) + 1u), ConstantU32(state, 20u)), ConstantU32(state, 0x1ffu)) + : state.module.SpecializationConstant(u32, PipelineSpecialization::FmaskFormatBase + resource, 0u); + auto layout = ConstantU32(state, 0u); + for (auto value = static_cast(IrBufferFormat::Fmask8_S2_F1); value <= static_cast(IrBufferFormat::Fmask64_S16_F8); ++value) { + const auto fmask = FmaskLayoutOf(static_cast(value)); + if (!fmask.has_value()) continue; + layout = Select(state, u32, Binary(state, spv::OpIEqual, TypeBool(state), format, ConstantU32(state, value)), ConstantU32(state, fmask->sampleBits | (fmask->samples << 8u) | (fmask->fragments << 16u)), layout); + } + const auto sampleBits = Binary(state, spv::OpBitwiseAnd, u32, layout, ConstantU32(state, 0xffu)); + const auto samples = Binary(state, spv::OpBitwiseAnd, u32, Binary(state, spv::OpShiftRightLogical, u32, layout, ConstantU32(state, 8u)), ConstantU32(state, 0xffu)); + const auto fragments = Binary(state, spv::OpShiftRightLogical, u32, layout, ConstantU32(state, 16u)); + const auto packed = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u32, packed, texel, 0u); + auto expanded = ConstantU32(state, 0u); + for (std::uint32_t sample = 0; sample < 8u; ++sample) { + const auto field = state.module.AllocateId(); + state.module.AddFunction(spv::OpBitFieldUExtract, u32, field, packed, Binary(state, spv::OpIMul, u32, sampleBits, ConstantU32(state, sample)), sampleBits); + const auto fragment = Select(state, u32, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), field, fragments), ConstantU32(state, 8u), field); + const auto nibble = Select(state, u32, Binary(state, spv::OpULessThan, TypeBool(state), ConstantU32(state, sample), samples), fragment, ConstantU32(state, 0u)); + expanded = Binary(state, spv::OpBitwiseOr, u32, expanded, Binary(state, spv::OpShiftLeftLogical, u32, nibble, ConstantU32(state, sample * 4u))); + } + const std::array components{expanded, ConstantU32(state, 0u), ConstantU32(state, 0u), ConstantU32(state, 0u)}; + std::uint32_t selected[4] = {}; + for (std::uint32_t component = 0; component < 4u; component++) selected[component] = RuntimeSwizzledComponent(state, RuntimeImageSwizzle(state, resource, component), components); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), result, selected[0], selected[1], selected[2], selected[3]); + return result; +} + std::uint32_t UnpackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t texel) { auto& state = ctx.state; if (access.image.srgbDecode) { return DecodeSrgbTexel(state, texel); } + if (access.image.conversionFormat == FmaskExpansion) { + return ExpandFmaskTexel(state, access, texel); + } const auto info = ImageConversionFormat(access.image); if (info.format == IrBufferFormat::Invalid) { return texel; diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index df60e02b6b..448f187bd3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -46,7 +46,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) whose key is neither a V#-relative scalar read nor a value the SRT walker evaluates (for example a shifted or masked runtime-offset V# read, a per-lane value made uniform by `v_readfirstlane`, or a loop-carried phi) binds every record of the snapshot domain instead of the selected ones. So does a table read at `(key << n) & mask` with such a key: a key domain would select records by the unmasked key, so that key is not narrowed. A key the walker evaluates selects its one record instead, unless the shader may write the memory the key is read from at run time (the same buffer, or any memory when the read or a write goes through a pointer or a GPU-only descriptor); then the column binds the whole domain too. A write to the key's memory through another V# over the same bytes is not detected. A shader whose selected records would pass 64 images or 64 sampled image/sampler pairs binds, for the heaps with the most such reads, the union of their keys in one table instead. - [Image table pointer indexing](../../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) propagates 32-bit value bounds through `ReadFirstLane` and `SelectU32` so waterfall key masks keep their bit domain. The SRT walker validates runtime select conditions whose active lane mask implies the condition through `LogicalAnd` conjunctions, assuming unselected inactive lanes need not be uniform across the full wave. The same assumption lets the two `s_load`s of one T# record match when one offset is added before `v_readfirstlane` and the other after (`ReadFirstLane(Select(mask, (key << 5) + 16, old), mask)` against `ReadFirstLane(key, mask) << 5`): with no active lane the hardware reads lane 0 of the inactive arm, and the table still binds the whole record. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. -- Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read uncompressed `Fmask32_S8_F8` through a single-mip 2D or 2D-array `R32_UINT` snapshot with `Z_64KB_X` addressing. Destination selectors may name X, zero or one. Other FMASK formats, CMASK compression, packed accesses and other layouts throw. The layout follows PAL's [FMASK-to-surface conversion](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877) and [32-bit view selectors](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Device.cpp#L4973). Native RDNA/PS5 image-load results and title rendering are unmeasured; compressed color sampling remains unsupported. +- Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) of 16 samples, CMASK compression (bar the 8-fragment native owner path), packed accesses and other layouts throw. Uncompressed FMASK of up to 8 samples reads a single-mip 2D or 2D-array 8-, 16- or 32-bit `UINT` snapshot with `Z_64KB_X` addressing; selectors may name X, zero or one. `Fmask32_S8_F8` returns the stored word; others expand to 4 bits per sample, an index at or past the fragment count reading 8 (unknown), samples past the count 0. Layouts follow PAL's [expanded values](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MaskRam.cpp#L5005) and [FMASK surfaces](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877). Console results, unused nibbles included, are unmeasured; compressed color sampling is unsupported. - A [DIM 2D read of a 2D MSAA T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) returns sample 0, which is not measured on the console's gfx1013, at an implicit LOD above 0, at an explicit one for texels over 16 bits except formats 22 and 56 in SW_64KB_R_X, or for other formats and swizzle modes; 64- and 128-bit SW_64KB_Z_X texels are not covered: gfx1031 does not address their samples. Such a read is an `image_load` without a mip, `image_sample`, `image_sample_lz` or `image_sample_l` of a direct sampled image; other accesses (gathers, bias, gradients, offsets, LOD clamps, depth compares, `image_load_mip`, `image_get_resinfo`, `image_get_lod`, PCK, BY2/BY4) throw, as does a depth-bits T# (16_UINT, 32_UINT or 32_SINT in a Z swizzle mode). On gfx1031 (RX 6700 XT, HIP), such reads return sample 0 at 2x, 4x and 8x for 8- to 128-bit texels in SW_64KB_R_X and 8- to 32-bit ones in SW_64KB_Z_X (formats 1, 5, 7, 11, 13, 14, 18, 22, 29, 56, 60, 64, 71, 77, 133). - `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. From 287b149950b69b1fee370a147906c081fb0974bd Mon Sep 17 00:00:00 2001 From: mazen Date: Mon, 19 Oct 2026 16:24:29 +0400 Subject: [PATCH 272/318] fix(shader): keep an unbounded pixel LDS array in a storage buffer A Function array of 8192 dwords faults NVIDIA driver 617.42. The guest array stays that size, and each invocation gets its own region in a buffer. --- .../Execution/src/VulkanDevice.cpp | 2 + .../libSceAgcDriver/Graphics/include/Draw.hpp | 11 ++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 160 +++++++++++++++++ .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 12 +- .../Graphics/src/ShaderValidation.cpp | 5 + .../tests/execution/PixelLdsLimit.cpp | 165 ++++++++++++++++++ core/shader/recompiler/Recompiler.cpp | 10 +- core/shader/recompiler/Recompiler.hpp | 3 +- core/shader/recompiler/ShaderDiskCache.cpp | 2 + .../include/SpirvBackend/SpirvAnalysis.hpp | 2 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 10 +- .../src/SpirvMemory/SpirvBufferAccess.cpp | 50 +++++- .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 8 + .../src/SpirvMemory/SpirvModuleSetup.cpp | 4 +- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 2 +- docs/dev/debt/shader.md | 2 +- 18 files changed, 436 insertions(+), 14 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index fe3802c146..c6e39b0346 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -29,6 +29,7 @@ #include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp" #include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" @@ -2284,6 +2285,7 @@ void VulkanDevice::WaitIdle() { state->recorder->Sync(); } check(state->DeviceFunction("vkDeviceWaitIdle")(state->device), "vkDeviceWaitIdle"); + Graphics::CheckPixelFunctionLds(graphicsContext()); APS5_LOG_CHARS_OUT_DEBUG("VulkanDevice::WaitIdle complete"); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp index 535e799c9a..e2ffa0f98d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Draw.hpp @@ -18,6 +18,17 @@ struct HostImport; // built for its draw-cache entry (design_cpu_final M8; null otherwise, and always under // APS5_NO_DRAW_RECIPE=1). void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span shaders, std::span snapshots = {}, std::shared_ptr* recipe = nullptr); + +struct PixelFunctionLdsLayouts { + VkDescriptorSetLayout empty = VK_NULL_HANDLE; + VkDescriptorSetLayout buffer = VK_NULL_HANDLE; +}; + +PixelFunctionLdsLayouts PixelFunctionLdsSetLayouts(const Context& context); +void PreparePixelFunctionLds(const Context& context, VkCommandBuffer commands, std::uint32_t subgroupSize); +void BindPixelFunctionLds(const Context& context, VkCommandBuffer commands, VkPipelineLayout layout); +void CheckPixelFunctionLds(const Context& context); +void ReleasePixelFunctionLds(VkDevice device); std::optional KnownValidationFailure(const Context& context, std::span shaders, const State& state); std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std::span targetViews); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index fc04136642..befeb99d09 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -20,6 +20,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Optimization/include/Optimization/ShaderStageInputInfo.hpp" +#include "SpirvBackend/SpirvAnalysis.hpp" #include "ThreadOwned.hpp" #include "prx/libc/include/HostThreadLocal.hpp" #include @@ -42,6 +43,159 @@ namespace AgcDriver::Graphics { +namespace { + +struct PixelFunctionLdsPool { + Context context{}; + VkBuffer buffer = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; + VkDescriptorSet set = VK_NULL_HANDLE; + VkDescriptorSetLayout empty = VK_NULL_HANDLE; + VkDescriptorSetLayout bufferLayout = VK_NULL_HANDLE; + std::uint32_t* header = nullptr; + std::uint32_t subgroupSize = 0; + std::uint32_t slots = 0; + bool announced = false; +}; + +std::mutex& PixelLdsMutex() { + static std::mutex mutex; + return mutex; +} + +std::unordered_map& PixelLdsPools() { + static std::unordered_map pools; + return pools; +} + +void DestroyPixelFunctionLds(PixelFunctionLdsPool& pool) { + if (pool.header != nullptr) pool.context.Function("vkUnmapMemory")(pool.context.device, pool.memory); + if (pool.descriptorPool != VK_NULL_HANDLE) pool.context.Function("vkDestroyDescriptorPool")(pool.context.device, pool.descriptorPool, nullptr); + if (pool.buffer != VK_NULL_HANDLE) pool.context.Function("vkDestroyBuffer")(pool.context.device, pool.buffer, nullptr); + if (pool.memory != VK_NULL_HANDLE) pool.context.Function("vkFreeMemory")(pool.context.device, pool.memory, nullptr); + if (pool.bufferLayout != VK_NULL_HANDLE) pool.context.Function("vkDestroyDescriptorSetLayout")(pool.context.device, pool.bufferLayout, nullptr); + if (pool.empty != VK_NULL_HANDLE) pool.context.Function("vkDestroyDescriptorSetLayout")(pool.context.device, pool.empty, nullptr); + pool.header = nullptr; + pool.descriptorPool = VK_NULL_HANDLE; + pool.set = VK_NULL_HANDLE; + pool.buffer = VK_NULL_HANDLE; + pool.memory = VK_NULL_HANDLE; + pool.bufferLayout = VK_NULL_HANDLE; + pool.empty = VK_NULL_HANDLE; +} + +} + +PixelFunctionLdsLayouts PixelFunctionLdsSetLayouts(const Context& context) { + std::lock_guard lock(PixelLdsMutex()); + auto& pool = PixelLdsPools()[context.device]; + if (pool.bufferLayout == VK_NULL_HANDLE) { + pool.context = context; + VkDescriptorSetLayoutCreateInfo emptyInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &emptyInfo, nullptr, &pool.empty), "vkCreateDescriptorSetLayout pixel function LDS empty"); + const VkDescriptorSetLayoutBinding binding{0u, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1u, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr}; + VkDescriptorSetLayoutCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO}; + info.bindingCount = 1u; + info.pBindings = &binding; + Check(context.Function("vkCreateDescriptorSetLayout")(context.device, &info, nullptr, &pool.bufferLayout), "vkCreateDescriptorSetLayout pixel function LDS"); + } + return {pool.empty, pool.bufferLayout}; +} + +void PreparePixelFunctionLds(const Context& context, VkCommandBuffer commands, std::uint32_t subgroupSize) { + const auto operations = VK_SUBGROUP_FEATURE_BASIC_BIT | VK_SUBGROUP_FEATURE_BALLOT_BIT; + Require((context.subgroup.supportedStages & VK_SHADER_STAGE_FRAGMENT_BIT) != 0 && (context.subgroup.supportedOperations & operations) == operations, "pixel function LDS needs fragment subgroup elect, ballot and broadcast"); + Require(subgroupSize != 0u && subgroupSize == context.subgroup.subgroupSize, "pixel function LDS subgroup size does not match the device"); + std::lock_guard lock(PixelLdsMutex()); + auto& pool = PixelLdsPools()[context.device]; + Require(pool.bufferLayout != VK_NULL_HANDLE, "pixel function LDS descriptor set layout was not created"); + if (pool.buffer == VK_NULL_HANDLE) { + const auto stride = static_cast(subgroupSize) * ShaderRecompiler::FunctionLdsDwordLimit * 4ull; + const auto budget = std::min(context.limits.maxStorageBufferRange, 2ull << 30); + Require(budget > 16ull + stride, "pixel function LDS does not fit in the device storage buffer range"); + pool.slots = static_cast((budget - 16ull) / stride); + pool.subgroupSize = subgroupSize; + const auto bytes = 16ull + stride * pool.slots; + VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; + bufferInfo.size = bytes; + bufferInfo.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + Check(context.Function("vkCreateBuffer")(context.device, &bufferInfo, nullptr, &pool.buffer), "vkCreateBuffer pixel function LDS"); + VkMemoryRequirements requirements{}; + context.Function("vkGetBufferMemoryRequirements")(context.device, pool.buffer, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + const auto allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &pool.memory); + if (allocated != VK_SUCCESS) { + context.Function("vkDestroyBuffer")(context.device, pool.buffer, nullptr); + pool.buffer = VK_NULL_HANDLE; + Check(allocated, "vkAllocateMemory pixel function LDS"); + } + Check(context.Function("vkBindBufferMemory")(context.device, pool.buffer, pool.memory, 0), "vkBindBufferMemory pixel function LDS"); + Check(context.Function("vkMapMemory")(context.device, pool.memory, 0, 16, 0, reinterpret_cast(&pool.header)), "vkMapMemory pixel function LDS"); + pool.header[0] = 0u; + pool.header[1] = 0u; + pool.header[2] = pool.slots; + const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1u}; + VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO}; + poolInfo.maxSets = 1u; + poolInfo.poolSizeCount = 1u; + poolInfo.pPoolSizes = &size; + Check(context.Function("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool.descriptorPool), "vkCreateDescriptorPool pixel function LDS"); + VkDescriptorSetAllocateInfo sets{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; + sets.descriptorPool = pool.descriptorPool; + sets.descriptorSetCount = 1u; + sets.pSetLayouts = &pool.bufferLayout; + Check(context.Function("vkAllocateDescriptorSets")(context.device, &sets, &pool.set), "vkAllocateDescriptorSets pixel function LDS"); + const VkDescriptorBufferInfo buffer{pool.buffer, 0, bytes}; + VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET}; + write.dstSet = pool.set; + write.dstBinding = 0u; + write.descriptorCount = 1u; + write.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + write.pBufferInfo = &buffer; + context.Function("vkUpdateDescriptorSets")(context.device, 1u, &write, 0u, nullptr); + std::fprintf(stderr, "[gpu] pixel function LDS pool: %u subgroups, %llu MiB, stride %u lanes\n", pool.slots, static_cast(bytes >> 20u), subgroupSize); + pool.announced = true; + } + Require(pool.subgroupSize == subgroupSize, "pixel function LDS subgroup size does not match the pool"); + context.Resolved(&DeviceFunctions::cmdFillBuffer, "vkCmdFillBuffer")(commands, pool.buffer, 0, 8, 0u); + const VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); +} + +std::uint32_t PixelFunctionLdsSubgroup(std::span shaders) { + std::uint32_t subgroup = 0u; + for (const auto& shader : shaders) { + if (shader.program != nullptr && shader.program->functionLdsBuffer) subgroup = shader.program->hostSubgroupSize; + } + return subgroup; +} + +void BindPixelFunctionLds(const Context& context, VkCommandBuffer commands, VkPipelineLayout layout) { + std::lock_guard lock(PixelLdsMutex()); + const auto& pool = PixelLdsPools().at(context.device); + Require(pool.set != VK_NULL_HANDLE, "pixel function LDS buffer was not prepared"); + context.Resolved(&DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, ShaderRecompiler::PixelFunctionLdsDescriptorSet, 1, &pool.set, 0, nullptr); +} + +void CheckPixelFunctionLds(const Context& context) { + std::lock_guard lock(PixelLdsMutex()); + const auto found = PixelLdsPools().find(context.device); + if (found == PixelLdsPools().end() || found->second.header == nullptr) return; + if (found->second.header[1] != 0u) throw std::runtime_error("AGC graphics: pixel function LDS subgroup slots exhausted"); +} + +void ReleasePixelFunctionLds(VkDevice device) { + std::lock_guard lock(PixelLdsMutex()); + const auto found = PixelLdsPools().find(device); + if (found == PixelLdsPools().end()) return; + DestroyPixelFunctionLds(found->second); + PixelLdsPools().erase(found); +} + std::uint64_t DrawRenderPassKey(const Context& context, const State& state, std::span targetViews) { std::uint64_t key = 14695981039346656037ull; const auto mix = [&](std::uint64_t value) { @@ -2068,6 +2222,8 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); countBarrier(1); } + const auto pixelLdsSubgroup = PixelFunctionLdsSubgroup(shaders); + if (pixelLdsSubgroup != 0u) PreparePixelFunctionLds(context, commands, pixelLdsSubgroup); if (meshIndirect) { meshArguments = recordMeshArguments(context, commands, recorder, true, state, draw, *record.indirect, countBarrier, inputs.restartTable); argumentBuffer = meshArguments->Handle(); @@ -2093,6 +2249,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } else { resources.Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, record.pipeline->Layout()); } + if (std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; })) BindPixelFunctionLds(context, commands, record.pipeline->Layout()); APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); pushDrawConstants(*record.pipeline, commands, state, draw, shaders, resources, record.pushBytes, record.pushStages, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty()); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); @@ -2766,6 +2923,8 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT); countBarrier(); } + const auto pixelLdsSubgroup = PixelFunctionLdsSubgroup(shaders); + if (pixelLdsSubgroup != 0u) PreparePixelFunctionLds(context, commands, pixelLdsSubgroup); std::shared_ptr meshArguments; if (state.stages.mesh && args != nullptr) { meshArguments = recordMeshArguments(context, commands, recorder, recorded, state, draw, indirect, countBarrier, inputs.restartTable); @@ -2807,6 +2966,7 @@ void recordPreparedDraw(const Context& context, const State& state, const Pm4::D pipeline->Begin(commands, *framebuffer, state.renderExtent, state); APS5_LOG_CHARS_OUT_DEBUG("Pipeline Begin OK"); resources->Bind(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline->Layout()); + if (pixelLdsSubgroup != 0u) BindPixelFunctionLds(context, commands, pipeline->Layout()); APS5_LOG_CHARS_OUT_DEBUG("Resources bound"); pushDrawConstants(*pipeline, commands, state, draw, shaders, *resources, nullptr, 0, meshArguments != nullptr ? meshArguments->DeviceAddress() : 0, !inputs.restartTable.empty()); APS5_LOG_CHARS_OUT_DEBUG("Push constants recorded"); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 9a2bf59a56..470b77f6f3 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -279,7 +280,7 @@ void appendKey(std::vector& key, const TValue& value) { bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span shaders) { using Stage = ShaderRecompiler::ShaderStage; - if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state)) return false; + if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state) || std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; })) return false; keys = {}; for (const auto& shader : shaders) { const auto identity = shader.program->Identity(); @@ -463,12 +464,14 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput } // A descriptor set layout with the same bindings as this one is compatible with the pipeline // layout, so later draws bind their own ShaderResources' set under it. - const std::array setLayouts{resources.main, resources.fragment}; + const bool pixelLds = std::ranges::any_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->functionLdsBuffer; }); + const auto pixelLdsLayouts = pixelLds ? PixelFunctionLdsSetLayouts(context) : PixelFunctionLdsLayouts{}; + const std::array setLayouts{resources.main, pixelLds && !resources.split ? pixelLdsLayouts.empty : resources.fragment, pixelLdsLayouts.buffer}; const VkPushConstantRange push{pushStages, 0, PipelinePushSlotBytes}; const std::array slots{{{pushStagesOf(context), 0, PipelinePushSlotBytes}, {VK_SHADER_STAGE_FRAGMENT_BIT, PipelinePushSlotBytes, PipelinePushSlotBytes}}}; VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; - layoutInfo.flags = resources.split ? VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT : 0; - layoutInfo.setLayoutCount = resources.split ? 2u : 1u; + layoutInfo.flags = pixelLds || !resources.split ? 0 : VK_PIPELINE_LAYOUT_CREATE_INDEPENDENT_SETS_BIT_EXT; + layoutInfo.setLayoutCount = pixelLds ? 3u : resources.split ? 2u : 1u; layoutInfo.pSetLayouts = setLayouts.data(); layoutInfo.pushConstantRangeCount = dynamicRendering ? static_cast(slots.size()) : pushStages != 0 ? 1 : 0; layoutInfo.pPushConstantRanges = dynamicRendering ? slots.data() : pushStages != 0 ? &push : nullptr; @@ -1232,6 +1235,7 @@ void ClearCachedPipelines(VkDevice device) { it = store.entries.erase(it); } ClearPipelineLibraries(device); + ReleasePixelFunctionLds(device); } } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index abcf13c57c..7dd97e4370 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -1,4 +1,5 @@ #include "BdaAbi.hpp" +#include "SpirvBackend/SpirvAnalysis.hpp" #include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp" #include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp" #include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp" @@ -594,6 +595,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi } else { Require(variable.storage == spv::StorageClassStorageBuffer && decoration.set && decoration.binding, "unsupported or unbound shader resource"); const auto key = std::make_pair(*decoration.set, *decoration.binding); + if (*decoration.set == ShaderRecompiler::PixelFunctionLdsDescriptorSet) { + Require(fragment && *decoration.binding == 0u, "pixel function LDS buffer binding is wrong"); + continue; + } const auto binding = std::find_if(shader.bindings.begin(), shader.bindings.end(), [&](const auto& item) { return item.descriptorSet == key.first && item.binding == key.second; }); const bool array = binding != shader.bindings.end() && binding->role == ShaderRecompiler::DescriptorRole::GuestBuffers; const auto [view, first] = bufferViews.emplace(key, decoration.aliased); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp new file mode 100644 index 0000000000..8ef17d4d0d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp @@ -0,0 +1,165 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Width = 64; +constexpr std::uint32_t Height = 32; +constexpr std::uint32_t High = 0x11110000u; +constexpr std::uint32_t Last = 0x22220000u; +constexpr std::uint32_t Past = 0x33330000u; +alignas(256) std::array Pixels{}; +alignas(256) std::array Results{}; + +alignas(256) constexpr std::array VertexCode{ + 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, +}; + +std::vector PixelCode() { + std::vector code{0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, Width}; + const auto mov = [&](std::uint32_t reg, std::uint32_t literal) { + code.push_back(0x7e0002ffu | (reg << 17u)); + code.push_back(literal); + }; + const auto shl = [&](std::uint32_t dest, std::uint32_t source) { + code.push_back(0x34000000u | (dest << 17u) | (source << 9u) | 0x82u); + }; + const auto write = [&](std::uint32_t index, std::uint32_t value) { + mov(16u, index); + shl(17u, 16u); + mov(18u, value); + code.push_back(0xd8340000u); + code.push_back((18u << 8u) | 17u); + }; + write(8000u, High); + write(8191u, Last); + write(8192u, Past); + code.push_back(0xbf8cc07fu); + const auto read = [&](std::uint32_t index, std::uint32_t dest) { + mov(16u, index); + shl(17u, 16u); + code.push_back(0xd8d80000u); + code.push_back((dest << 24u) | 17u); + }; + read(8000u, 10u); + read(8191u, 11u); + read(8192u, 12u); + shl(15u, 3u); + code.push_back(0xd8d80000u); + code.push_back((13u << 24u) | 15u); + code.push_back(0xbf8cc07fu); + for (std::uint32_t slot = 0; slot < 4u; ++slot) { + code.push_back(0xe0702000u | (slot * 4u)); + code.push_back(0x80010006u | ((10u + slot) << 8u)); + } + code.insert(code.end(), {0x7e0e02f2, 0xf800180f, 0x07070707, 0xbf810000}); + return code; +} + +std::array BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), count, 0x01016facu}; +} + +constexpr std::array, 3> Triangles{{ + {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} +}}; + +void Draw(AgcDriver::VulkanDevice& device) { + const auto pixelCode = PixelCode(); + Pixels.fill(std::byte{0}); + Results.fill(0u); + const auto target = device.Target(); + std::vector vertexUserData(4, 0u); + const auto vertexBuffer = BufferDescriptor(Triangles.data(), 16u, static_cast(Triangles.size())); + std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin()); + const std::array vertexMemory{{{reinterpret_cast(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}}; + ShaderRecompiler::RecompileRequest vertex{ + {ShaderStage::Vertex, reinterpret_cast(VertexCode.data()), VertexCode, 0, {}}, + {64u, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory}, + target, + {0, 0, 0, 64} + }; + vertex.useCache = false; + const auto vertexResult = ShaderRecompiler::Recompile(vertex); + const auto vertexPush = static_cast(vertexResult.pushConstants.size()); + ShaderRecompiler::ShaderPixelStageInfo pixel{}; + pixel.wave32 = false; + pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionX) | ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::PositionY); + pixel.posX = true; + pixel.posY = true; + pixel.targetOutputMode[0] = 9; + pixel.targetExportMapping.fill(0xe4u); + std::vector pixelUserData(8, 0u); + const auto results = BufferDescriptor(Results.data(), 16u, Width * Height); + std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(std::span(pixelCode))}}}; + ShaderRecompiler::RecompileRequest fragment{ + {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, + {64u, 0, pixelUserData, std::nullopt, pixel, std::nullopt, pixelMemory}, + target, + PixelPushLayout(vertexPush, target) + }; + fragment.useCache = false; + const auto pixelResult = ShaderRecompiler::Recompile(fragment); + Require(pixelResult.functionLdsBuffer, "pixel LDS limit: the full array was not backed by a buffer"); + const std::array shaders{{ + {ShaderStage::Vertex, &vertexResult, 0}, + {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} + }}; + AgcDriver::Graphics::State state{}; + state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; + state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.colors = {state.color}; + state.hasColorTarget = true; + state.renderExtent = {Width, Height}; + state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.negativeOneToOne = false; + state.scissor = {{0, 0}, {Width, Height}}; + state.cullMode = VK_CULL_MODE_NONE; + state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; + state.blend.colorWriteMask = 15; + state.blends = {state.blend}; + state.blendConstants = {}; + const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangles.size()), 0, 1, 0, false}; + device.Draw(state, draw, shaders); + device.WaitIdle(); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (device->Target().subgroupSize < 32u) { + std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); + return VulkanTestSkipped; + } + Draw(*device); + for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { + for (std::uint32_t channel = 0; channel < 4u; ++channel) Require(Pixels[pixel * 4u + channel] == std::byte{255}, "pixel LDS limit: missing color"); + const auto base = pixel * 4u; + Require(Results[base] == High, "pixel LDS limit: dword 8000 expected " + std::to_string(High) + " got " + std::to_string(Results[base])); + Require(Results[base + 1u] == Last, "pixel LDS limit: dword 8191 expected " + std::to_string(Last) + " got " + std::to_string(Results[base + 1u])); + Require(Results[base + 2u] == 0u, "pixel LDS limit: dword 8192 wrote " + std::to_string(Results[base + 2u])); + } + std::puts("pixel LDS limit passed: dword 8000 and 8191, 2048 pixels"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 24bc292f3f..0413a773da 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -650,14 +650,14 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) } catch (const SpannedWaveUnsupported& error) { std::fprintf(stderr, "[spanned-wave] 0x%llx falls back to two lanes per invocation: %s\n", static_cast(request.shader.codeAddress), error.what()); targetOptions.spannedWaves = false; - result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions, &emitted); } if (registration && result.spirv.size() > request.target.registrationWordLimit) { registration = false; program.Metadata().bindingLayoutComplete = false; bindings = bindingAllocator.Allocate(program, request.layout, false, stencilReplay); targetOptions.registrationModules = false; - result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions, &emitted); } result.registrationModule = registration; if (request.shader.stage == ShaderStage::Mesh && request.graphics) result.meshBarycentrics = request.graphics->meshBarycentrics; @@ -675,6 +675,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) result.runtimeImageCount = bindings.layout.runtimeImageCount; result.hostSubgroupSize = HostSubgroupSize(request); result.fullSubgroups = emitted.fullSubgroups; + result.functionLdsBuffer = emitted.functionLdsBuffer; result.vertexOffsetSgpr = program.Info().vertexOffsetSgpr; result.instanceOffsetSgpr = program.Info().instanceOffsetSgpr; result.vertexOffsetShared = program.Info().vertexOffsetShared; @@ -835,11 +836,13 @@ std::shared_ptr buildSpecializedModule(const CompiledSh #endif auto module = std::make_shared(); std::map bindingNumbers; + std::unordered_set functionLdsBuffer; for (std::size_t cursor = 5; cursor < materialized.size();) { const auto count = materialized[cursor] >> 16u; const auto op = static_cast(materialized[cursor] & 0xffffu); if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationBinding) bindingNumbers.emplace(materialized[cursor + 1u], materialized[cursor + 3u]); - if (op == spv::OpVariable && count >= 4u) { + if (op == spv::OpDecorate && count == 4u && materialized[cursor + 2u] == spv::DecorationDescriptorSet && materialized[cursor + 3u] == PixelFunctionLdsDescriptorSet) functionLdsBuffer.insert(materialized[cursor + 1u]); + if (op == spv::OpVariable && count >= 4u && !functionLdsBuffer.contains(materialized[cursor + 2u])) { if (const auto found = bindingNumbers.find(materialized[cursor + 2u]); found != bindingNumbers.end()) module->bindings.push_back(found->second); module->pushData |= materialized[cursor + 3u] == spv::StorageClassPushConstant; } @@ -1003,6 +1006,7 @@ RecompileResult materializeResult(const CompiledVariant& variant, const Recompil .instanceOffsetConflict = artifact.instanceOffsetConflict, .hostSubgroupSize = artifact.hostSubgroupSize, .fullSubgroups = artifact.fullSubgroups, + .functionLdsBuffer = artifact.functionLdsBuffer, .parameterExports = artifact.parameterExports, .fragmentParameters = artifact.fragmentParameters, .barycentricEmulation = artifact.barycentricEmulation, diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index e8ffba43e1..b523040560 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -672,6 +672,7 @@ struct CompiledShaderArtifact { bool instanceOffsetConflict = false; std::uint32_t hostSubgroupSize = 0; bool fullSubgroups = false; + bool functionLdsBuffer = false; std::vector parameterExports; std::vector fragmentParameters; BarycentricEmulation barycentricEmulation; @@ -741,7 +742,7 @@ struct RecompileResult : CompiledShaderArtifact, ShaderInvocation { }; mix(specialization.size()); for (const auto& constant : specialization) mix(constant.id | (static_cast(constant.value) << 32u)); - mix(hostSubgroupSize | (fullSubgroups ? 0x10000u : 0u) | (static_cast(workgroupMemoryDwords) << 32u)); + mix(hostSubgroupSize | (fullSubgroups ? 0x10000u : 0u) | (functionLdsBuffer ? 0x20000u : 0u) | (static_cast(workgroupMemoryDwords) << 32u)); mix(bindings.size()); for (const auto& binding : bindings) mix(binding.descriptorSet | (static_cast(binding.binding) << 16u) | (static_cast(binding.kind) << 40u) | (static_cast(binding.count) << 48u)); if (identity.Empty()) identity.low = 1; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index ee1ba3838f..481c5d232b 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -495,6 +495,7 @@ void encodeArtifact(Writer& writer, const CompiledShaderArtifact& result) { writer.Value(result.instanceOffsetConflict); writer.Value(result.hostSubgroupSize); writer.Value(result.fullSubgroups); + writer.Value(result.functionLdsBuffer); writer.Values(std::span(result.parameterExports)); writer.List(result.fragmentParameters, [](Writer& out, const FragmentParameter& parameter) { out.Value(parameter.location); @@ -556,6 +557,7 @@ void decodeArtifact(Reader& reader, CompiledShaderArtifact& result) { reader.Value(result.instanceOffsetConflict); reader.Value(result.hostSubgroupSize); reader.Value(result.fullSubgroups); + reader.Value(result.functionLdsBuffer); reader.Values(result.parameterExports); reader.List(result.fragmentParameters, 10, [](Reader& in, FragmentParameter& parameter) { in.Value(parameter.location); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index ee435ffc67..2dbe8f7d03 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -39,6 +39,8 @@ struct SpirvRequirements { [[nodiscard]] SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program); inline constexpr std::uint32_t FunctionLdsDwordLimit = 8192u; +inline constexpr std::uint32_t PixelFunctionStorageDwords = 4096u; +inline constexpr std::uint32_t PixelFunctionLdsDescriptorSet = 2u; [[nodiscard]] std::uint32_t FunctionLdsDwords(const IrProgram& program); [[nodiscard]] std::unordered_map FunctionLdsLaneAddresses(const IrProgram& program); [[nodiscard]] std::unordered_set WaveUniformValues(const IrProgram& program); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 74d6b2e281..f43c064182 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -43,6 +43,7 @@ struct SpirvTargetOptions { struct SpirvEmitInfo { bool fullSubgroups = false; + bool functionLdsBuffer = false; }; struct SpannedWaveUnsupported : std::runtime_error { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 31a5981434..38b321e595 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -206,6 +206,7 @@ struct SpirvEmitterState { std::uint32_t ldsBufferVariable = 0; std::uint32_t ldsBufferU64Variable = 0; std::uint32_t ldsBufferBase = 0; + bool pixelFunctionLdsBuffer = false; std::uint32_t numWorkgroupsVariable = 0; std::array scratchVariable {}; std::array vectorWindowVariable {}; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 55aa8b12a7..4939cb02e8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -281,6 +281,11 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.float32DenormPreserve = target.float32DenormPreserve; state.fusedExactArithmetic = target.fusedExactArithmetic; state.hostSubgroupSize = target.subgroupSize; + if (program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > PixelFunctionStorageDwords) { + state.pixelFunctionLdsBuffer = true; + state.requirements.subgroupLocalInvocationId = true; + state.requirements.subgroupBallot = true; + } if (state.requirements.ldsLock && LdsInDeviceMemory(state) && std::find(target.supportedCapabilities.begin(), target.supportedCapabilities.end(), static_cast(spv::CapabilityInt64Atomics)) != target.supportedCapabilities.end()) { state.requirements.ldsLock = false; state.requirements.ldsBufferInt64Atomics = true; @@ -319,7 +324,10 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh EmitModuleHeader(state, bindings); EmitProgram(state); state.module.EmitEntryPoint(ExecutionModelForStage(state.program.Resources().stage), state.mainFunc, "main", state.interfaceVariables); - if (info != nullptr) info->fullSubgroups = state.spannedWave; + if (info != nullptr) { + info->fullSubgroups = state.spannedWave; + info->functionLdsBuffer = state.pixelFunctionLdsBuffer; + } return state.module.Finalize(); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 192b60cc27..c405971431 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -70,10 +70,49 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state) { } std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state) { - return spv::MemorySemanticsWorkgroupMemoryMask | (LdsInDeviceMemory(state) ? spv::MemorySemanticsUniformMemoryMask : 0u); + return spv::MemorySemanticsWorkgroupMemoryMask | (LdsInDeviceMemory(state) || state.pixelFunctionLdsBuffer ? spv::MemorySemanticsUniformMemoryMask : 0u); +} + +void EmitPixelFunctionLdsBase(SpirvEmitterState& state) { + if (!state.pixelFunctionLdsBuffer) return; + if (state.ldsBufferVariable == 0u || state.hostSubgroupSize == 0u) FailEmit("pixel function LDS buffer was not declared"); + const auto element = [&](std::uint32_t dword) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state), pointer, state.ldsBufferVariable, ConstantU32(state, 0u), ConstantU32(state, dword)); + return pointer; + }; + const auto slotVar = state.module.AllocateId(); + state.module.AddFunction(spv::OpVariable, TypePointer(state, spv::StorageClassFunction, TypeU32(state)), slotVar, spv::StorageClassFunction, ConstantU32(state, 0u)); + const auto elected = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformElect, TypeBool(state), elected, ConstantU32(state, spv::ScopeSubgroup)); + EmitIfCondition(state, elected, [&] { + const auto acquired = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicIAdd, TypeU32(state), acquired, element(0u), ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | spv::MemorySemanticsUniformMemoryMask), ConstantU32(state, 1u)); + const auto capacity = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), capacity, element(2u)); + const auto exhausted = state.module.AllocateId(); + state.module.AddFunction(spv::OpUGreaterThanEqual, TypeBool(state), exhausted, acquired, capacity); + EmitIfCondition(state, exhausted, [&] { + state.module.AddFunction(spv::OpAtomicStore, element(1u), ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsReleaseMask | spv::MemorySemanticsUniformMemoryMask), ConstantU32(state, 1u)); + }); + const auto claimed = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelect, TypeU32(state), claimed, exhausted, ConstantU32(state, 0u), acquired); + state.module.AddFunction(spv::OpStore, slotVar, claimed); + }); + const auto ballot = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBallot, TypeU32Vector(state, 4u), ballot, ConstantU32(state, spv::ScopeSubgroup), elected); + const auto source = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBallotFindLSB, TypeU32(state), source, ConstantU32(state, spv::ScopeSubgroup), ballot); + const auto local = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), local, slotVar); + const auto slot = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBroadcast, TypeU32(state), slot, ConstantU32(state, spv::ScopeSubgroup), local, source); + const auto within = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, slot, ConstantU32(state, state.hostSubgroupSize)), EmitSubgroupLocalInvocationId(state)); + state.ldsBufferBase = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, within, ConstantU32(state, FunctionLdsDwordLimit)), ConstantU32(state, 3u)); } void EmitLdsBufferBase(SpirvEmitterState& state) { + EmitPixelFunctionLdsBase(state); if (!LdsInDeviceMemory(state)) { return; } @@ -125,6 +164,12 @@ MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const access.kind = mem.kind; switch (mem.kind) { case ResourceKind::Lds: + if (state.pixelFunctionLdsBuffer) { + if (state.ldsBufferVariable == 0u || state.ldsBufferBase == 0u) FailEmit("pixel function LDS buffer was not prepared at function entry"); + access.objectPointer = state.ldsBufferVariable; + access.length = ConstantU32(state, LdsDwordCount(state)); + return access; + } if (LdsInDeviceMemory(state)) { if (state.ldsBufferVariable == 0u || state.ldsBufferBase == 0u) { FailEmit("device memory LDS was not prepared at function entry"); @@ -203,6 +248,9 @@ std::uint32_t GuardedElementIndex(SpirvEmitterState& state, const MemoryResource } std::uint32_t EmitMemoryElementPointer(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index) { + if (access.kind == ResourceKind::Lds && state.pixelFunctionLdsBuffer) { + return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state)); + } if (access.kind == ResourceKind::Lds && LdsInDeviceMemory(state)) { return EmitStorageBufferElementPointer(state, access, EmitAddU32(state, state.ldsBufferBase, index), TypeStorageBufferElementPointer(state)); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index accb81d256..a090a68541 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -574,6 +574,14 @@ void DefineDescriptors(SpirvEmitterState& state) { for (const auto variable : {state.ldsBufferVariable, state.ldsBufferU64Variable}) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); } } + if (state.pixelFunctionLdsBuffer) { + state.ldsBufferVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassStorageBuffer, StorageBufferBlockType(state)), spv::StorageClassStorageBuffer); + state.module.AddName(state.ldsBufferVariable, "pixel_function_lds"); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationDescriptorSet, PixelFunctionLdsDescriptorSet); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationBinding, 0u); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationCoherent); + if (state.spirvVersion >= 0x00010400u) state.interfaceVariables.push_back(state.ldsBufferVariable); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index ddde52c8e9..ea47f0b3af 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -216,7 +216,7 @@ void DefineModule(SpirvEmitterState& state) { DefineOutputs(state); DefineTessellationInterfaces(state); DefineDescriptors(state); - if (state.requirements.functionLds) { + if (state.requirements.functionLds && !state.pixelFunctionLdsBuffer) { state.ldsVariable = state.module.AllocateId(); } if (state.requirements.functionScratch) { @@ -402,7 +402,7 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDerivativeGroupLinearKHR); } state.module.AddName(state.mainFunc, "main"); - if (state.requirements.functionLds) { + if (state.requirements.functionLds && !state.pixelFunctionLdsBuffer) { state.module.AddName(state.ldsVariable, "lds_dwords"); } if (state.requirements.functionScratch) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 4f558e43a1..b9f6c7b5d5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1552,7 +1552,7 @@ void EmitProgram(SpirvEmitterState& state) { } state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.meshGuestFunc != 0u ? state.meshGuestFunc : state.pixelGuestFunc != 0u ? state.pixelGuestFunc : state.mainFunc, spv::FunctionControlMaskNone, TypeFunction(state)); EmitLabel(state, state.entryLabel); - if (state.requirements.functionLds) { + if (state.requirements.functionLds && !state.pixelFunctionLdsBuffer) { state.module.AddFunction(spv::OpVariable, TypeU32ArrayPointer(state, spv::StorageClassFunction, LdsStorageDwords(state)), state.ldsVariable, spv::StorageClassFunction); } if (state.requirements.functionScratch) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e66c9cec39..6d88203131 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,7 +151,7 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but is backed by a storage buffer, one 8192-dword region per invocation, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); the buffer holds as many in-flight subgroups as fit in the smaller of 2 GiB and the device's storage-buffer range, and a draw that needs another slot sets a flag that `WaitIdle` throws on instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. From d37e9f4ea77e1cb45f8e2b9e1662c60fa514a119 Mon Sep 17 00:00:00 2001 From: mazen Date: Mon, 19 Oct 2026 21:47:06 +0400 Subject: [PATCH 273/318] fix(shader): index pixel LDS by the host lane A wave32 pixel program on a wider subgroup was using the guest lane, so both halves of that subgroup wrote one region. --- .../SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index c405971431..30ce436187 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -107,7 +107,13 @@ void EmitPixelFunctionLdsBase(SpirvEmitterState& state) { state.module.AddFunction(spv::OpLoad, TypeU32(state), local, slotVar); const auto slot = state.module.AllocateId(); state.module.AddFunction(spv::OpGroupNonUniformBroadcast, TypeU32(state), slot, ConstantU32(state, spv::ScopeSubgroup), local, source); - const auto within = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, slot, ConstantU32(state, state.hostSubgroupSize)), EmitSubgroupLocalInvocationId(state)); + std::uint32_t lane; + if (state.splitSubgroup) { + if (state.subgroupLocalInvocationIdVariable == 0) FailEmit("pixel function LDS lane was not declared"); + lane = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), lane, state.subgroupLocalInvocationIdVariable); + } else lane = EmitSubgroupLocalInvocationId(state); + const auto within = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, slot, ConstantU32(state, state.hostSubgroupSize)), lane); state.ldsBufferBase = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, within, ConstantU32(state, FunctionLdsDwordLimit)), ConstantU32(state, 3u)); } From dba7f5466c64defe2f878f48d1579dab0c7f7e56 Mon Sep 17 00:00:00 2001 From: mazen Date: Mon, 19 Oct 2026 22:17:01 +0400 Subject: [PATCH 274/318] fix(shader): keep a pixel LDS overflow until the GPU is idle The slot counter is reset only after earlier fragment writes, and the overflow flag stays until WaitIdle. The storage buffer is limited to NVIDIA's proprietary driver, which is where the large function array does not finish. --- .../Execution/src/VulkanDevice.cpp | 3 ++ .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 12 +++-- .../tests/execution/PixelLdsLimit.cpp | 44 ++++++++++++------ core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 6 ++- core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 2 +- .../tests/RequestSerializerTests.cpp | 46 +++++++++++++------ docs/dev/debt/shader.md | 3 +- 11 files changed, 83 insertions(+), 37 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c6e39b0346..a642fba48a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -342,6 +342,7 @@ struct VulkanDevice::State { VkDeviceSize uploadSize = 0; VkPhysicalDeviceProperties properties{}; std::string driverInfo; + VkDriverId driverId = VK_DRIVER_ID_MAX_ENUM; bool individuallyFreeDescriptorSets = false; PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr; @@ -1262,6 +1263,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &driverProperties); driverId = driver.driverID; state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo; + state->driverId = driver.driverID; state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); @@ -3781,6 +3783,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.registrationModules = state->graphicsPipelineLibrary; target.robustBufferAccess2 = state->robustBufferAccess2; target.waveLdsMemoryBarriers = !state->subgroupControlBarriers; + target.largePixelFunctionLdsBuffer = state->driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY; target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index befeb99d09..104520cf80 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -161,9 +161,11 @@ void PreparePixelFunctionLds(const Context& context, VkCommandBuffer commands, s pool.announced = true; } Require(pool.subgroupSize == subgroupSize, "pixel function LDS subgroup size does not match the pool"); - context.Resolved(&DeviceFunctions::cmdFillBuffer, "vkCmdFillBuffer")(commands, pool.buffer, 0, 8, 0u); - const VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; - context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 1, &barrier, 0, nullptr, 0, nullptr); + const VkMemoryBarrier beforeFill{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &beforeFill, 0, nullptr, 0, nullptr); + context.Resolved(&DeviceFunctions::cmdFillBuffer, "vkCmdFillBuffer")(commands, pool.buffer, 0, 4, 0u); + const VkMemoryBarrier afterFill{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT}; + context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 1, &afterFill, 0, nullptr, 0, nullptr); } std::uint32_t PixelFunctionLdsSubgroup(std::span shaders) { @@ -185,7 +187,9 @@ void CheckPixelFunctionLds(const Context& context) { std::lock_guard lock(PixelLdsMutex()); const auto found = PixelLdsPools().find(context.device); if (found == PixelLdsPools().end() || found->second.header == nullptr) return; - if (found->second.header[1] != 0u) throw std::runtime_error("AGC graphics: pixel function LDS subgroup slots exhausted"); + const auto exhausted = found->second.header[1] != 0u; + found->second.header[1] = 0u; + if (exhausted) throw std::runtime_error("AGC graphics: pixel function LDS subgroup slots exhausted"); } void ReleasePixelFunctionLds(VkDevice device) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp index 8ef17d4d0d..9299392bb5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include #include namespace { @@ -21,13 +23,16 @@ constexpr std::uint32_t Last = 0x22220000u; constexpr std::uint32_t Past = 0x33330000u; alignas(256) std::array Pixels{}; alignas(256) std::array Results{}; +constexpr std::uint32_t Overflow = 256u; +alignas(256) std::array OverflowPixels{}; +alignas(256) std::array OverflowResults{}; alignas(256) constexpr std::array VertexCode{ 0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000, }; -std::vector PixelCode() { - std::vector code{0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, Width}; +std::vector PixelCode(std::uint32_t width) { + std::vector code{0x7e040f00, 0x7e060f01, 0xd5430006, 0x0409ff03, width}; const auto mov = [&](std::uint32_t reg, std::uint32_t literal) { code.push_back(0x7e0002ffu | (reg << 17u)); code.push_back(literal); @@ -76,10 +81,10 @@ constexpr std::array, 3> Triangles{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -void Draw(AgcDriver::VulkanDevice& device) { - const auto pixelCode = PixelCode(); - Pixels.fill(std::byte{0}); - Results.fill(0u); +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t width, std::uint32_t height, std::span pixels, std::span results) { + const auto pixelCode = PixelCode(width); + std::fill(pixels.begin(), pixels.end(), std::byte{0}); + std::fill(results.begin(), results.end(), 0u); const auto target = device.Target(); std::vector vertexUserData(4, 0u); const auto vertexBuffer = BufferDescriptor(Triangles.data(), 16u, static_cast(Triangles.size())); @@ -102,8 +107,8 @@ void Draw(AgcDriver::VulkanDevice& device) { pixel.targetOutputMode[0] = 9; pixel.targetExportMapping.fill(0xe4u); std::vector pixelUserData(8, 0u); - const auto results = BufferDescriptor(Results.data(), 16u, Width * Height); - std::copy(results.begin(), results.end(), pixelUserData.begin() + 4); + const auto stored = BufferDescriptor(results.data(), 16u, width * height); + std::copy(stored.begin(), stored.end(), pixelUserData.begin() + 4); const std::array pixelMemory{{{reinterpret_cast(pixelCode.data()), std::as_bytes(std::span(pixelCode))}}}; ShaderRecompiler::RecompileRequest fragment{ {ShaderStage::Fragment, reinterpret_cast(pixelCode.data()), pixelCode, 0, {}}, @@ -113,21 +118,21 @@ void Draw(AgcDriver::VulkanDevice& device) { }; fragment.useCache = false; const auto pixelResult = ShaderRecompiler::Recompile(fragment); - Require(pixelResult.functionLdsBuffer, "pixel LDS limit: the full array was not backed by a buffer"); + Require(pixelResult.functionLdsBuffer == target.largePixelFunctionLdsBuffer, target.largePixelFunctionLdsBuffer ? "pixel LDS limit: the full array was not backed by a buffer" : "pixel LDS limit: a driver that keeps the function array used the buffer"); const std::array shaders{{ {ShaderStage::Vertex, &vertexResult, 0}, {ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)} }}; AgcDriver::Graphics::State state{}; state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, 64u, 64u, std::nullopt, std::nullopt}; - state.color = {reinterpret_cast(Pixels.data()), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, Pixels.size(), 0xe4u}; + state.color = {reinterpret_cast(pixels.data()), {width, height}, VK_FORMAT_R8G8B8A8_UNORM, pixels.size(), 0xe4u}; state.colors = {state.color}; state.hasColorTarget = true; - state.renderExtent = {Width, Height}; + state.renderExtent = {width, height}; state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - state.viewport = {0, static_cast(Height), static_cast(Width), -static_cast(Height), 0, 1}; + state.viewport = {0, static_cast(height), static_cast(width), -static_cast(height), 0, 1}; state.negativeOneToOne = false; - state.scissor = {{0, 0}, {Width, Height}}; + state.scissor = {{0, 0}, {width, height}}; state.cullMode = VK_CULL_MODE_NONE; state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; state.blend.colorWriteMask = 15; @@ -148,7 +153,7 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } - Draw(*device); + Draw(*device, Width, Height, Pixels, Results); for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { for (std::uint32_t channel = 0; channel < 4u; ++channel) Require(Pixels[pixel * 4u + channel] == std::byte{255}, "pixel LDS limit: missing color"); const auto base = pixel * 4u; @@ -157,6 +162,17 @@ int main() { Require(Results[base + 2u] == 0u, "pixel LDS limit: dword 8192 wrote " + std::to_string(Results[base + 2u])); } std::puts("pixel LDS limit passed: dword 8000 and 8191, 2048 pixels"); + if (device->Target().largePixelFunctionLdsBuffer) { + bool threw = false; + try { + Draw(*device, Overflow, Overflow, OverflowPixels, OverflowResults); + } catch (const std::exception& error) { + threw = std::string_view(error.what()) == "AGC graphics: pixel function LDS subgroup slots exhausted"; + if (!threw) throw; + } + Require(threw, "pixel LDS limit: a draw past the subgroup slots did not throw"); + std::puts("pixel LDS limit overflow passed"); + } return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index fdcff33207..cf4bbc0138 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -298,6 +298,7 @@ private: append(key, value.waveLdsMemoryBarriers); append(key, value.float32DenormPreserve); append(key, value.fusedExactArithmetic); + append(key, value.largePixelFunctionLdsBuffer); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 96bb58e1a5..9457081b29 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -832,7 +832,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(44u); + writer.WriteU32(45u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -871,6 +871,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const writer.WriteU32(std::bit_cast(request.context.vertex->pointSizeMax)); } writer.WriteBool(request.target.fusedExactArithmetic); + writer.WriteBool(request.target.largePixelFunctionLdsBuffer); return base64Encode(buffer); } @@ -879,7 +880,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 44u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 45u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -934,6 +935,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.vertex = vertex; } if (version >= 44u) result.request.target.fusedExactArithmetic = reader.ReadBool(); + if (version >= 45u) result.request.target.largePixelFunctionLdsBuffer = reader.ReadBool(); if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 0413a773da..2204a0db94 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -606,6 +606,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.float32FlushToZero = request.target.float32FlushToZero; targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; targetOptions.fusedExactArithmetic = request.target.fusedExactArithmetic; + targetOptions.largePixelFunctionLdsBuffer = request.target.largePixelFunctionLdsBuffer; targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { const auto& paired = *request.graphics->meshBarycentrics; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index b523040560..978d400225 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -299,6 +299,7 @@ struct SpirvTarget { bool waveLdsMemoryBarriers = false; bool float32DenormPreserve = false; bool fusedExactArithmetic = false; + bool largePixelFunctionLdsBuffer = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index f43c064182..4cb86e2472 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -39,6 +39,7 @@ struct SpirvTargetOptions { std::vector meshExportedLocations; std::vector> meshPerVertexLocations; bool waveLdsMemoryBarriers = false; + bool largePixelFunctionLdsBuffer = false; }; struct SpirvEmitInfo { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 4939cb02e8..0cfb1a7be2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -281,7 +281,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.float32DenormPreserve = target.float32DenormPreserve; state.fusedExactArithmetic = target.fusedExactArithmetic; state.hostSubgroupSize = target.subgroupSize; - if (program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > PixelFunctionStorageDwords) { + if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > PixelFunctionStorageDwords) { state.pixelFunctionLdsBuffer = true; state.requirements.subgroupLocalInvocationId = true; state.requirements.subgroupBallot = true; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index eb8d34ad6d..4ebf8833b1 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -219,13 +219,14 @@ int main() { Require(request.graphics->draw.indexAddress == 0xabcdef3400u && request.graphics->draw.indexCount == 57u && request.graphics->draw.indexElementBytes == 4u && request.graphics->draw.instanceCount == 9u); Require(request.target.float16DenormPreserve == preserve); auto upgraded = bytes; - upgraded[4] = 44u; + upgraded[4] = 45u; InsertMeshBarycentricFlag(upgraded); if (fixture.pixel) upgraded.insert(upgraded.begin() + 258u, {0u, 0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(upgraded); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 6u, 0u); AppendVertexTail(upgraded); upgraded.push_back(0u); + upgraded.push_back(0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -260,13 +261,14 @@ int main() { layerRejected = std::string_view(error.what()) == "RequestSerializer: truncated data"; } Require(layerRejected); - layered[4] = 44u; + layered[4] = 45u; InsertMeshBarycentricFlag(layered); layered.insert(layered.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(layered); if (fixture.tessellation) layered.insert(layered.end() - 35u, 6u, 0u); AppendVertexTail(layered); layered.push_back(0u); + layered.push_back(0u); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -287,12 +289,13 @@ int main() { Require(replay.request.context.pixel->rasterPrimitive == ShaderRecompiler::RasterPrimitive::Rectangle); Require(replay.request.context.vertex->paClVsOutCntl == 0x00050a05u); Require(replay.request.target.float16DenormPreserve == preserve); - bytes[4] = 44u; + bytes[4] = 45u; InsertMeshBarycentricFlag(bytes); bytes.insert(bytes.begin() + 259u, {0u, 0u}); InsertPixelDefaultsAfterFrontFace(bytes); AppendVertexTail(bytes); bytes.push_back(0u); + bytes.push_back(0u); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -418,12 +421,13 @@ int main() { if (fixture.pixel) main38.insert(main38.begin() + 259u, {0u, 0u}); if (fixture.pixel) InsertPixelDefaultsAfterFrontFace(main38, false); auto upgraded = main38; - upgraded[4] = 44u; + upgraded[4] = 45u; if (fixture.pixel) upgraded.insert(upgraded.begin() + 317u, 16u, 0u); if (fixture.tessellation) upgraded.insert(upgraded.end() - 35u, 1u, 0u); main38.insert(main38.end(), sizeof(std::uint64_t) + 1u + 5u + 1u, 0u); AppendVertexTail(upgraded); upgraded.push_back(0u); + upgraded.push_back(0u); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -439,8 +443,11 @@ int main() { pointed.context.vertex->pointSize = 3.0f; pointed.context.vertex->pointSizeMin = 1.0f; pointed.context.vertex->pointSizeMax = 8.0f; - auto version44 = DecodeBase64(serializer.Serialize(pointed)); - Require(version44.size() > 15u && version44[4] == 44u); + auto version45 = DecodeBase64(serializer.Serialize(pointed)); + Require(version45.size() > 15u && version45[4] == 45u); + auto version44 = version45; + version44.pop_back(); + version44[4] = 44u; auto version43 = version44; version43.pop_back(); version43[4] = 43u; @@ -471,7 +478,7 @@ int main() { const auto replay25 = serializer.Deserialize(EncodeBase64(main25)); Require(!fixture.pixel || (replay25.request.context.pixel->layerExported == layer && replay25.request.context.pixel->ditheredAlphaToCoverage == dither && replay25.request.context.pixel->reversedBlend == 0u)); Require(replay25.request.target.float16DenormPreserve == preserve && (!fixture.tessellation || replay25.request.graphics->tessellation->primitiveGroupSize == 37u)); - main25[4] = 44u; + main25[4] = 45u; InsertMeshBarycentricFlag(main25); if (fixture.pixel) { main25.insert(main25.begin() + 260u, 0u); @@ -480,6 +487,7 @@ int main() { if (fixture.tessellation) main25.insert(main25.end() - 35u, 1u, 0u); AppendVertexTail(main25); main25.push_back(0u); + main25.push_back(0u); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -491,19 +499,21 @@ int main() { request.target.float16DenormPreserve = preserve; request.target.float32FlushToZero = !preserve; request.target.float32DenormPreserve = preserve; + request.target.largePixelFunctionLdsBuffer = !preserve; const auto encoded = serializer.Serialize(request); const auto replay = serializer.Deserialize(encoded); - Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve); + Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve); auto bytes = DecodeBase64(encoded); - Require(bytes.size() >= 8u && bytes[4] == 44u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); + Require(bytes.size() >= 8u && bytes[4] == 45u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; version40.pop_back(); version40.pop_back(); + version40.pop_back(); version40[4] = 40u; const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 45u; + future[4] = 46u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -550,6 +560,7 @@ int main() { Require(version41.end() - order > static_cast(sizeof(pixel.sampleOrder)) && *order == 0x10u); version41.erase(order, order + sizeof(pixel.sampleOrder)); version41.pop_back(); + version41.pop_back(); version41[4] = 41u; const auto replay41 = serializer.Deserialize(EncodeBase64(version41)); Require(replay41.request.context.pixel.has_value() && replay41.request.context.pixel->sampleLocations == pixel.sampleLocations && replay41.request.context.pixel->sampleOrder == std::array{}); @@ -561,6 +572,7 @@ int main() { request.target.float32FlushToZero = true; auto version35 = DecodeBase64(serializer.Serialize(request)); version35.pop_back(); + version35.pop_back(); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto pixelTail = static_cast(1u + sizeof(pixel.sampleOrder)); Require(version35.end() - stencilOperationValue > pixelTail && std::all_of(stencilOperationValue, stencilOperationValue + pixelTail, [](std::uint8_t value) { return value == 0u; })); @@ -605,7 +617,7 @@ int main() { for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { pointRequest.target.spannedWaves = spanned; auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); - legacy.resize(legacy.size() - (version >= 36u ? 15u : version >= 35u ? 16u : 21u)); + legacy.resize(legacy.size() - (version >= 36u ? 16u : version >= 35u ? 17u : 22u)); legacy[4] = version; const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); Require(replayLegacy.request.target.spannedWaves == spanned); @@ -617,7 +629,7 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 44u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 15u : version >= 35u ? 16u : 21u)); + Require(upgradedLegacy[4] == 45u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 16u : version >= 35u ? 17u : 22u)); } } @@ -626,14 +638,14 @@ int main() { const auto sampledPoint = serializer.Deserialize(serializer.Serialize(pointRequest)); Require(sampledPoint.request.context.vertex->pointList && sampledPoint.request.context.pixel->sampleOrder == pixel.sampleOrder); auto legacy42 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy42.resize(legacy42.size() - 14u); + legacy42.resize(legacy42.size() - 15u); legacy42[4] = 42u; const auto replay42 = serializer.Deserialize(EncodeBase64(legacy42)); Require(replay42.request.context.pixel->sampleOrder == pixel.sampleOrder && !replay42.request.context.vertex->pointList && replay42.request.context.vertex->pointSize == 1.0f); pixel.sampleOrder = {}; pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy34.resize(legacy34.size() - 21u); + legacy34.resize(legacy34.size() - 22u); const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto legacyPixelTail = static_cast(1u + sizeof(pixel.sampleOrder)); Require(legacy34.end() - stencil34 > legacyPixelTail && std::all_of(stencil34, stencil34 + legacyPixelTail, [](std::uint8_t value) { return value == 0u; })); @@ -645,8 +657,12 @@ int main() { for (const bool fused : {false, true}) { request.target.fusedExactArithmetic = fused; + request.target.largePixelFunctionLdsBuffer = false; auto bytes = DecodeBase64(serializer.Serialize(request)); - Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes.back() == (fused ? 1u : 0u)); + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 2u] == (fused ? 1u : 0u) && bytes.back() == 0u); + bytes.pop_back(); + bytes[4] = 44u; + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused); bytes[4] = 43u; bytes.pop_back(); Require(!serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 6d88203131..fe570ca6e3 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,7 +151,8 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but is backed by a storage buffer, one 8192-dword region per invocation, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); the buffer holds as many in-flight subgroups as fit in the smaller of 2 GiB and the device's storage-buffer range, and a draw that needs another slot sets a flag that `WaitIdle` throws on instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a flag that stays set until `WaitIdle` throws instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. + - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. From d06caa7a8de8e2ad68629f33ea857a1ba0aa320b Mon Sep 17 00:00:00 2001 From: Maz Date: Tue, 20 Oct 2026 13:56:45 +0400 Subject: [PATCH 275/318] fix(shader): update request fixtures for pixel LDS version 39 Unsupported-version probes use 40, and legacy downgrade fixtures drop the new trailer bool so Deserialize no longer sees trailing data. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index effc0dc179..9aa30d1018 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5289,6 +5289,7 @@ void verifyVertexInfoVersion19() { auto version32 = decodeBase64(serializer.Serialize(spanned)); version32.pop_back(); version32.pop_back(); + version32.pop_back(); require(version32.size() > 2u && version32.back() == 0u && version32[version32.size() - 2u] == 0u && version32[version32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); version32.pop_back(); version32.pop_back(); @@ -5389,11 +5390,12 @@ void verifyDepthRemapRequests() { const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); constexpr std::size_t pointTailBytes = 13u; - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 8u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 8 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 1 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 1 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail and the fused arithmetic flag of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 9u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 9 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 2 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 2 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag and the pixel function LDS flag of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; legacyBytes.pop_back(); legacyBytes.pop_back(); + legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes); const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); @@ -5403,6 +5405,7 @@ void verifyDepthRemapRequests() { auto version35 = remappedBytes; version35.pop_back(); version35.pop_back(); + version35.pop_back(); version35[4] = 35u; version35.resize(version35.size() - 1u - pointTailBytes); const auto replay35 = serializer.Deserialize(encodeBase64(version35)); From 89fcff6dd8ad76eab49ffc4e3637bb5df13e30fc Mon Sep 17 00:00:00 2001 From: Maz Date: Tue, 20 Oct 2026 15:38:04 +0400 Subject: [PATCH 276/318] fix(shader): broadcast the first pixel LDS slot without a dynamic lane --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 4 ++-- .../SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 9aa30d1018..167de17f59 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5453,8 +5453,8 @@ void verifyMergedHullConfiguration() { require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged"); constexpr std::size_t pointTailBytes = 13u; auto legacy = decodeBase64(merged); - require(legacy.size() > 54u + pointTailBytes && legacy[4] == 44u, "a merged hull request did not use serialization version 44"); - legacy.resize(legacy.size() - 1u - pointTailBytes); + require(legacy.size() > 54u + pointTailBytes && legacy[4] == 45u, "a merged hull request did not use serialization version 45"); + legacy.resize(legacy.size() - 2u - pointTailBytes); legacy.pop_back(); require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); legacy.erase(legacy.end() - 52); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 30ce436187..1a0ebafe6d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -99,14 +99,10 @@ void EmitPixelFunctionLdsBase(SpirvEmitterState& state) { state.module.AddFunction(spv::OpSelect, TypeU32(state), claimed, exhausted, ConstantU32(state, 0u), acquired); state.module.AddFunction(spv::OpStore, slotVar, claimed); }); - const auto ballot = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBallot, TypeU32Vector(state, 4u), ballot, ConstantU32(state, spv::ScopeSubgroup), elected); - const auto source = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBallotFindLSB, TypeU32(state), source, ConstantU32(state, spv::ScopeSubgroup), ballot); const auto local = state.module.AllocateId(); state.module.AddFunction(spv::OpLoad, TypeU32(state), local, slotVar); const auto slot = state.module.AllocateId(); - state.module.AddFunction(spv::OpGroupNonUniformBroadcast, TypeU32(state), slot, ConstantU32(state, spv::ScopeSubgroup), local, source); + state.module.AddFunction(spv::OpGroupNonUniformBroadcastFirst, TypeU32(state), slot, ConstantU32(state, spv::ScopeSubgroup), local); std::uint32_t lane; if (state.splitSubgroup) { if (state.subgroupLocalInvocationIdVariable == 0) FailEmit("pixel function LDS lane was not declared"); From fcf4a098780bf589e359de951ea7138306831bf8 Mon Sep 17 00:00:00 2001 From: Maz Date: Wed, 21 Oct 2026 10:39:16 +0400 Subject: [PATCH 277/318] fix(shader): begin a pass before preparing pixel LDS Continued draws skipped the slot fill while still binding the set. Main already uses serialization version 41, so the LDS flag is 42. --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 104520cf80..b291f76dac 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -2179,7 +2179,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam } const PassAccess passAccess{passReads, passWrites, passImages, passAttachments, unknownMemory, unknownWrites, passWriteOnly}; const bool fenced = state.depth.has_value() || !record.proxies.empty(); - const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess); + const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect || PixelFunctionLdsSubgroup(shaders) != 0u, fenced, passAccess); for (const auto& target : record.multisampleTargets) KeepMultisampleTarget_nid_no_patch(target); const bool continued = start.continued; outcome.passContinued = continued; From 641a0a4173d8da5c81b97ae6749a1be722493cac Mon Sep 17 00:00:00 2001 From: Maz Date: Wed, 21 Oct 2026 16:01:21 +0400 Subject: [PATCH 278/318] fix(shader): check pixel LDS overflow when presents retire Exhausted subgroups share slot 0 until the sticky flag is read; check it on present retire as well as WaitIdle, and cover two draws into one target. --- .../prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 3 +++ .../prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp | 8 ++++---- docs/dev/debt/shader.md | 5 ++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index a642fba48a..0febf9d6f9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3656,6 +3656,7 @@ double VulkanDevice::RetirePresents(std::size_t keepInFlight) { const auto status = state->DeviceFunction("vkGetFenceStatus"); auto inFlight = static_cast(std::count_if(slots.begin(), slots.end(), [](const auto& slot) { return slot.inFlight; })); double waitedMs = 0; + bool retired = false; // Oldest first from the cursor; the fences of one queue signal in submission order, so the // first unsignaled one ends the scan, and the ones beyond the bound are the oldest. for (std::size_t i = 0; i < slots.size() && inFlight != 0; ++i) { @@ -3674,9 +3675,11 @@ double VulkanDevice::RetirePresents(std::size_t keepInFlight) { } const bool dump = slot.dumpRecorded; state->RetireSlot(slot); + retired = true; if (dump) timing.Mark("dump"); --inFlight; } + if (retired) Graphics::CheckPixelFunctionLds(graphicsContext()); return waitedMs; } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp index 9299392bb5..594281d148 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PixelLdsLimit.cpp @@ -81,7 +81,7 @@ constexpr std::array, 3> Triangles{{ {-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f} }}; -void Draw(AgcDriver::VulkanDevice& device, std::uint32_t width, std::uint32_t height, std::span pixels, std::span results) { +void Draw(AgcDriver::VulkanDevice& device, std::uint32_t width, std::uint32_t height, std::span pixels, std::span results, std::uint32_t draws = 1u) { const auto pixelCode = PixelCode(width); std::fill(pixels.begin(), pixels.end(), std::byte{0}); std::fill(results.begin(), results.end(), 0u); @@ -139,7 +139,7 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t width, std::uint32_t he state.blends = {state.blend}; state.blendConstants = {}; const AgcDriver::Pm4::DrawParameters draw{0, static_cast(Triangles.size()), 0, 1, 0, false}; - device.Draw(state, draw, shaders); + for (std::uint32_t n = 0; n < draws; ++n) device.Draw(state, draw, shaders); device.WaitIdle(); } @@ -153,7 +153,7 @@ int main() { std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize); return VulkanTestSkipped; } - Draw(*device, Width, Height, Pixels, Results); + Draw(*device, Width, Height, Pixels, Results, 2u); for (std::uint32_t pixel = 0; pixel < Width * Height; ++pixel) { for (std::uint32_t channel = 0; channel < 4u; ++channel) Require(Pixels[pixel * 4u + channel] == std::byte{255}, "pixel LDS limit: missing color"); const auto base = pixel * 4u; @@ -161,7 +161,7 @@ int main() { Require(Results[base + 1u] == Last, "pixel LDS limit: dword 8191 expected " + std::to_string(Last) + " got " + std::to_string(Results[base + 1u])); Require(Results[base + 2u] == 0u, "pixel LDS limit: dword 8192 wrote " + std::to_string(Results[base + 2u])); } - std::puts("pixel LDS limit passed: dword 8000 and 8191, 2048 pixels"); + std::puts("pixel LDS limit passed: dword 8000 and 8191, 2048 pixels, two draws"); if (device->Target().largePixelFunctionLdsBuffer) { bool threw = false; try { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index fe570ca6e3..76b39dd32c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,10 +151,13 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a flag that stays set until `WaitIdle` throws instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a sticky overflow flag and reuses slot 0 for the excess subgroups (sharing that region with the subgroup that owns it); `CheckPixelFunctionLds` throws when the flag is set, from `WaitIdle` and when a present fence retires, then clears it; arrays of 4097 to 6143 dwords take the buffer path though only 4096 (completes), 6144 (timeout), 7680 and 8192 (fault) were measured on that 5090; the pool is the first `HOST_VISIBLE|HOST_COHERENT` type so the host can map the 16-byte header without a separate device-local body (on discrete NVIDIA that is usually system memory for the full allocation, and a device-local body plus host-visible header was not measured); on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a flag that stays set until `WaitIdle` throws instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. +- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. From 40c7ff888ca1def7c27d8f9eb27edbcd0e271e58 Mon Sep 17 00:00:00 2001 From: Maz Date: Wed, 21 Oct 2026 19:02:25 +0400 Subject: [PATCH 279/318] fix(shader): scale pixel LDS Function threshold by SM budget Max measured a 4 GiB private-memory ceiling on a 4090; use VK_NV_shader_sm_builtins so denser cards take the buffer path below 4096. --- .../Execution/src/VulkanDevice.cpp | 13 +++++++++++++ .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 15 ++++++++++++++- core/shader/recompiler/CacheKey.hpp | 1 + .../ControlFlow/src/RequestSerializer.cpp | 1 + core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../recompiler/SpirvBackend/src/SpirvEmitter.cpp | 2 +- .../recompiler/tests/RequestSerializerTests.cpp | 5 ++++- docs/dev/debt/shader.md | 5 +---- 10 files changed, 38 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 0febf9d6f9..8c3637d70b 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -343,6 +343,7 @@ struct VulkanDevice::State { VkPhysicalDeviceProperties properties{}; std::string driverInfo; VkDriverId driverId = VK_DRIVER_ID_MAX_ENUM; + std::uint32_t pixelFunctionStorageDwords = 4096u; bool individuallyFreeDescriptorSets = false; PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr; PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr; @@ -1268,6 +1269,17 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); } + if (driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY && hasExtension(VK_NV_SHADER_SM_BUILTINS_EXTENSION_NAME)) { + VkPhysicalDeviceShaderSMBuiltinsPropertiesNV smBuiltins{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SM_BUILTINS_PROPERTIES_NV}; + VkPhysicalDeviceProperties2 smProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &smBuiltins}; + state->InstanceFunction("vkGetPhysicalDeviceProperties2")(selected, &smProperties); + const auto subgroup = state->subgroup.subgroupSize; + if (smBuiltins.shaderSMCount != 0u && smBuiltins.shaderWarpsPerSM != 0u && subgroup != 0u) { + const auto invocations = static_cast(smBuiltins.shaderSMCount) * smBuiltins.shaderWarpsPerSM * subgroup; + const auto budgetDwords = (4ull << 30) / (invocations * 4ull); + if (budgetDwords < state->pixelFunctionStorageDwords) state->pixelFunctionStorageDwords = static_cast(budgetDwords); + } + } auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); auto bdaFeatures = QueryBdaFeatures(selected, state->InstanceFunction("vkGetPhysicalDeviceFeatures2"), availableExtensions); require(hasExtension(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME), "VK_KHR_shader_float_controls is unavailable"); @@ -3787,6 +3799,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.robustBufferAccess2 = state->robustBufferAccess2; target.waveLdsMemoryBarriers = !state->subgroupControlBarriers; target.largePixelFunctionLdsBuffer = state->driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY; + target.pixelFunctionStorageDwords = state->pixelFunctionStorageDwords; target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 167de17f59..877cf34683 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5176,6 +5176,7 @@ void verifyVertexInfoVersion19() { require(offset + 4u <= plain.size() && flagged[offset] == 0xefu && flagged[offset + 1u] == 0xcdu && flagged[offset + 2u] == 0xabu && flagged[offset + 3u] == 0x89u, "PA_CL_VS_OUT_CNTL is not the four bytes the version 20 vertex info adds"); require(std::equal(plain.begin() + offset + 4u, plain.end(), flagged.begin() + offset + 4u), "PA_CL_VS_OUT_CNTL moved the fields after it"); auto version20Vertex = flagged; + version20Vertex.resize(version20Vertex.size() - sizeof(std::uint32_t)); version20Vertex.pop_back(); version20Vertex.pop_back(); version20Vertex.pop_back(); @@ -5188,6 +5189,7 @@ void verifyVertexInfoVersion19() { const auto layered = serializer.Deserialize(serializer.Serialize(vertex)); require(layered.request.context.vertex.has_value() && layered.request.context.vertex->paClVsOutCntl == vertex.context.vertex->paClVsOutCntl, "a layered vertex info lost its clip distances or render target index"); auto legacy = plain; + legacy.resize(legacy.size() - sizeof(std::uint32_t)); legacy.pop_back(); legacy.pop_back(); legacy.pop_back(); @@ -5236,6 +5238,7 @@ void verifyVertexInfoVersion19() { const auto orderedBytes = decodeBase64(serializer.Serialize(ordered)); require(orderedBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + static_cast(orderOffset), orderedBytes.begin()) && orderedBytes[orderOffset] == 0x10u && orderedBytes[orderOffset + 11u] == 0x76u && orderedBytes[orderOffset + 12u] == 0x01u && std::equal(plainPixel.begin() + static_cast(orderOffset + sampleOrderBytes), plainPixel.end(), orderedBytes.begin() + static_cast(orderOffset + sampleOrderBytes)), "the sample order is not the four words the version 42 pixel info adds after the stencil operation value export flag"); auto orderedVersion34 = orderedBytes; + orderedVersion34.resize(orderedVersion34.size() - sizeof(std::uint32_t)); orderedVersion34.pop_back(); orderedVersion34.pop_back(); orderedVersion34.pop_back(); @@ -5245,7 +5248,7 @@ void verifyVertexInfoVersion19() { const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); auto orderedVersion30 = orderedBytes; - orderedVersion30.resize(orderedVersion30.size() - 5u); + orderedVersion30.resize(orderedVersion30.size() - 5u - sizeof(std::uint32_t)); orderedVersion30.erase(orderedVersion30.begin() + static_cast(blendOffset + 4u), orderedVersion30.begin() + static_cast(orderOffset + sampleOrderBytes)); orderedVersion30[4] = 30u; const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); @@ -5253,6 +5256,7 @@ void verifyVertexInfoVersion19() { auto orderedSpanned = ordered; orderedSpanned.target.spannedWaves = true; auto orderedVersion32 = decodeBase64(serializer.Serialize(orderedSpanned)); + orderedVersion32.resize(orderedVersion32.size() - sizeof(std::uint32_t)); orderedVersion32.pop_back(); orderedVersion32.pop_back(); require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); @@ -5275,6 +5279,7 @@ void verifyVertexInfoVersion19() { const auto sampledBytes = decodeBase64(serializer.Serialize(sampledPixel)); require(std::equal(maskedBytes.begin(), maskedBytes.begin() + static_cast(maskOffset + 1u), sampledBytes.begin()) && sampledBytes[maskOffset + 1u] == 0x1u && sampledBytes[maskOffset + 5u] == 0x2u && sampledBytes[maskOffset + 9u] == 0x4u && sampledBytes[maskOffset + 13u] == 0x8u, "the quad sample masks are not the four words after the quad pixel mask"); auto version30 = sampledBytes; + version30.resize(version30.size() - sizeof(std::uint32_t)); version30.pop_back(); version30.pop_back(); version30.pop_back(); @@ -5287,6 +5292,7 @@ void verifyVertexInfoVersion19() { auto spanned = sampledPixel; spanned.target.spannedWaves = true; auto version32 = decodeBase64(serializer.Serialize(spanned)); + version32.resize(version32.size() - sizeof(std::uint32_t)); version32.pop_back(); version32.pop_back(); version32.pop_back(); @@ -5298,6 +5304,7 @@ void verifyVertexInfoVersion19() { const auto replay32 = serializer.Deserialize(encodeBase64(version32)); require(replay32.request.context.pixel.has_value() && !replay32.request.context.pixel->iteratesSamples && replay32.request.context.pixel->sampleLocations == std::array{} && replay32.request.target.spannedWaves && serializer.Serialize(replay32.request) == serializer.Serialize(spanned), "a version 32 request read pixel iteration or sample locations, or lost its spanned-waves flag"); auto version29 = sampledBytes; + version29.resize(version29.size() - sizeof(std::uint32_t)); version29.pop_back(); version29.pop_back(); version29.pop_back(); @@ -5305,6 +5312,7 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); auto version36 = sampledBytes; + version36.resize(version36.size() - sizeof(std::uint32_t)); version36.pop_back(); version36.pop_back(); version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); @@ -5315,6 +5323,7 @@ void verifyVertexInfoVersion19() { const auto replay29 = serializer.Deserialize(encodeBase64(version29)); require(replay29.request.context.pixel.has_value() && replay29.request.context.pixel->quadSampleMask == std::array{0x1u, 0x2u, 0x4u, 0x8u} && replay29.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay29.request)) == sampledBytes, "a version 29 request read a reversed-order blend code or misread the fields after the quad sample masks"); auto version28 = sampledBytes; + version28.resize(version28.size() - sizeof(std::uint32_t)); version28.pop_back(); version28.pop_back(); version28.pop_back(); @@ -5325,6 +5334,7 @@ void verifyVertexInfoVersion19() { const auto replay28 = serializer.Deserialize(encodeBase64(version28)); require(replay28.request.context.pixel.has_value() && replay28.request.context.pixel->quadPixelMask == 0x2u && replay28.request.context.pixel->quadSampleMask == std::array{0xffffu, 0xffffu, 0xffffu, 0xffffu} && replay28.request.context.pixel->reversedBlend == 0u && decodeBase64(serializer.Serialize(replay28.request)) == maskedBytes, "a version 28 request read quad sample masks or a reversed-order blend code, or misread the fields after them"); auto version27 = maskedBytes; + version27.resize(version27.size() - sizeof(std::uint32_t)); version27.pop_back(); version27.pop_back(); version27.pop_back(); @@ -5335,6 +5345,7 @@ void verifyVertexInfoVersion19() { const auto replay27 = serializer.Deserialize(encodeBase64(version27)); require(replay27.request.context.pixel.has_value() && replay27.request.context.pixel->quadPixelMask == 0xfu && replay27.request.context.pixel->reversedBlend == 0u && serializer.Serialize(replay27.request) == encodedPixel, "a version 27 request read a quad pixel mask or reversed-order blend code, or misread the fields after it"); auto legacyPixel = plainPixel; + legacyPixel.resize(legacyPixel.size() - sizeof(std::uint32_t)); legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); @@ -5393,6 +5404,7 @@ void verifyDepthRemapRequests() { require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 9u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 9 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 2 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 2 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag and the pixel function LDS flag of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; + legacyBytes.resize(legacyBytes.size() - sizeof(std::uint32_t)); legacyBytes.pop_back(); legacyBytes.pop_back(); legacyBytes.pop_back(); @@ -5403,6 +5415,7 @@ void verifyDepthRemapRequests() { require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); } auto version35 = remappedBytes; + version35.resize(version35.size() - sizeof(std::uint32_t)); version35.pop_back(); version35.pop_back(); version35.pop_back(); diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index cf4bbc0138..10aaf73ff4 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -299,6 +299,7 @@ private: append(key, value.float32DenormPreserve); append(key, value.fusedExactArithmetic); append(key, value.largePixelFunctionLdsBuffer); + append(key, value.pixelFunctionStorageDwords); } }; diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 9457081b29..c754c198e6 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -872,6 +872,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const } writer.WriteBool(request.target.fusedExactArithmetic); writer.WriteBool(request.target.largePixelFunctionLdsBuffer); + writer.WriteU32(request.target.pixelFunctionStorageDwords); return base64Encode(buffer); } diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index 2204a0db94..a310aa792d 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -607,6 +607,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) targetOptions.float32DenormPreserve = request.target.float32DenormPreserve; targetOptions.fusedExactArithmetic = request.target.fusedExactArithmetic; targetOptions.largePixelFunctionLdsBuffer = request.target.largePixelFunctionLdsBuffer; + targetOptions.pixelFunctionStorageDwords = request.target.pixelFunctionStorageDwords; targetOptions.meshBarycentricPixel = request.shader.stage == ShaderStage::Fragment && request.graphics && request.graphics->mesh; if (request.shader.stage == ShaderStage::Mesh && request.graphics && request.graphics->meshBarycentrics) { const auto& paired = *request.graphics->meshBarycentrics; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 978d400225..962ddf0d89 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -300,6 +300,7 @@ struct SpirvTarget { bool float32DenormPreserve = false; bool fusedExactArithmetic = false; bool largePixelFunctionLdsBuffer = false; + std::uint32_t pixelFunctionStorageDwords = 4096u; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 4cb86e2472..9a418524a3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -40,6 +40,7 @@ struct SpirvTargetOptions { std::vector> meshPerVertexLocations; bool waveLdsMemoryBarriers = false; bool largePixelFunctionLdsBuffer = false; + std::uint32_t pixelFunctionStorageDwords = 4096u; }; struct SpirvEmitInfo { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 0cfb1a7be2..8fd7e67442 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -281,7 +281,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.float32DenormPreserve = target.float32DenormPreserve; state.fusedExactArithmetic = target.fusedExactArithmetic; state.hostSubgroupSize = target.subgroupSize; - if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > PixelFunctionStorageDwords) { + if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > target.pixelFunctionStorageDwords) { state.pixelFunctionLdsBuffer = true; state.requirements.subgroupLocalInvocationId = true; state.requirements.subgroupBallot = true; diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 4ebf8833b1..7e55864b42 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -500,12 +500,14 @@ int main() { request.target.float32FlushToZero = !preserve; request.target.float32DenormPreserve = preserve; request.target.largePixelFunctionLdsBuffer = !preserve; + request.target.pixelFunctionStorageDwords = preserve ? 2048u : 4096u; const auto encoded = serializer.Serialize(request); const auto replay = serializer.Deserialize(encoded); - Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve); + Require(replay.request.target.float16DenormPreserve == preserve && replay.request.target.float32FlushToZero == !preserve && replay.request.target.float32DenormPreserve == preserve && replay.request.target.largePixelFunctionLdsBuffer == !preserve && replay.request.target.pixelFunctionStorageDwords == request.target.pixelFunctionStorageDwords); auto bytes = DecodeBase64(encoded); Require(bytes.size() >= 8u && bytes[4] == 45u && bytes[5] == 0u && bytes[6] == 0u && bytes[7] == 0u); auto version40 = bytes; + version40.resize(version40.size() - sizeof(std::uint32_t)); version40.pop_back(); version40.pop_back(); version40.pop_back(); @@ -571,6 +573,7 @@ int main() { Require(serializer.Deserialize(serializer.Serialize(request)).request.target.spannedWaves == spanned); request.target.float32FlushToZero = true; auto version35 = DecodeBase64(serializer.Serialize(request)); + version35.resize(version35.size() - sizeof(std::uint32_t)); version35.pop_back(); version35.pop_back(); const auto stencilOperationValue = std::search(version35.begin(), version35.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 76b39dd32c..40b1730a16 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,13 +151,10 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a sticky overflow flag and reuses slot 0 for the excess subgroups (sharing that region with the subgroup that owns it); `CheckPixelFunctionLds` throws when the flag is set, from `WaitIdle` and when a present fence retires, then clears it; arrays of 4097 to 6143 dwords take the buffer path though only 4096 (completes), 6144 (timeout), 7680 and 8192 (fault) were measured on that 5090; the pool is the first `HOST_VISIBLE|HOST_COHERENT` type so the host can map the 16-byte header without a separate device-local body (on discrete NVIDIA that is usually system memory for the full allocation, and a device-local body plus host-visible header was not measured); on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The pool fits subgroups in min(2 GiB, storage range); overflow sets a sticky flag checked from `WaitIdle` and present retirement. Header mapped via the first `HOST_VISIBLE|HOST_COHERENT` type. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - is an array private to each host invocation, in `Function` storage, as only compute and mesh programs get workgroup memory; a pixel array larger than 4096 dwords stays that size (8192 when an address has no bound) but on NVIDIA's proprietary driver is backed by a storage buffer, one 8192-dword region per host lane, because a `Function` array of 6144 or more dwords does not finish on NVIDIA driver 617.42 (RTX 5090: 4096 completes, 6144 times out, 7680 and 8192 fault the device); other drivers keep the `Function` array; the buffer holds as many subgroups of one draw as fit in the smaller of 2 GiB and the device's storage-buffer range (2047 subgroups of 32 on that 5090, so a 1920x1080 draw throws), the slot counter is reset for the next draw only after earlier fragment writes, and a draw that needs another slot sets a flag that stays set until `WaitIdle` throws instead of wrapping or dropping the write; on the console one LDS region is shared by the wave (RDNA 2 ISA sections 3.6.6 and 10.4), so a lane does not see another lane's LDS writes. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms), the 32-bit LDS atomics, `ds_append` and `ds_consume` load, update and store the invocation's own dword without an atomic, as Vulkan allows atomics only on `Uniform`, `Workgroup`, `Image`, `StorageBuffer`, `PhysicalStorageBuffer` and `TaskPayloadWorkgroupEXT` pointers (VUID-StandaloneSpirv-None-04686). `ds_append` and `ds_consume` update only the first active lane's copy and return its old value to every lane, so a second append after the first active lane changes, or a read of the counter by another lane, differs from the hardware. Not measured on hardware. -- [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes and `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as measured on gfx1035 (Linux, alignment mode UNALIGNED; the console's mode is unknown). Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) with `buffer_atomic_add`, `buffer_atomic_swap` and `buffer_atomic_cmpswap` and bases 0 to 3 bytes past a DWORD boundary with offsets 0 to 7, an address on a DWORD boundary updates that DWORD and returns its old value, and any other address stops the dispatch, which the recompiler reports as an unaligned access fault. -- [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD ("the two LSBs of the byte-address are ignored, thus forcing Dword alignment"). The mode comes from `SH_MEM_CONFIG.alignment_mode`, and the console's mode is unknown. MXGP 26 (PPSA30839) sets bits with `buffer_atomic_or` at odd byte offsets, so the console is not in a strict mode, where section 3.11 reports a memory violation for a misaligned atomic. In mode UNALIGNED, which Linux KFD sets, a misaligned atomic stops the dispatch (measured on gfx1031 and gfx1036), so the console is not in that mode either. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). - [32-bit bitfield masks](../../../core/shader/recompiler/Translation/src/IntegerInstructions.cpp) (`S_BFM_B32`, `V_BFM_B32`) use the truncating 32-bit shift specified by the [RDNA 1.0 ISA](https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna-shader-instruction-set-architecture.pdf), sections 12.1 (opcode 36) and 12.12 (opcode 867). Width and offset use their low five bits. This matches gfx1035 (Linux) for every width/offset pair, with and without upper operand bits set, and both leave SCC unchanged; not measured on the console. - [Vector buffer offsets that are not a multiple of 4](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - dword accesses count whole dwords from the V# base, where the hardware reads the exact bytes at base + offset. - [Raw `buffer_load_dwordx2`](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. From aa77a5ba98e4e510a3ef3b164a85ed4d53acb101 Mon Sep 17 00:00:00 2001 From: Maz Date: Fri, 23 Oct 2026 11:25:44 +0400 Subject: [PATCH 280/318] fix(shader): prefer device-local host-visible pixel LDS memory Keep the NVIDIA-only pool mappable for the sticky overflow header while preferring a device-local host-visible type when the driver exposes one. --- core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp | 11 ++++++++++- docs/dev/debt/shader.md | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b291f76dac..b06370fd79 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -126,7 +126,16 @@ void PreparePixelFunctionLds(const Context& context, VkCommandBuffer commands, s context.Function("vkGetBufferMemoryRequirements")(context.device, pool.buffer, &requirements); VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; allocation.allocationSize = requirements.size; - allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); + constexpr auto hostCoherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; + constexpr auto deviceLocalHost = hostCoherent | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; + std::optional deviceLocalType; + for (std::uint32_t i = 0; i < context.memory.memoryTypeCount; ++i) { + if ((requirements.memoryTypeBits & (1u << i)) != 0 && (context.memory.memoryTypes[i].propertyFlags & deviceLocalHost) == deviceLocalHost) { + deviceLocalType = i; + break; + } + } + allocation.memoryTypeIndex = deviceLocalType.value_or(context.MemoryType(requirements.memoryTypeBits, hostCoherent)); const auto allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &pool.memory); if (allocated != VK_SUCCESS) { context.Function("vkDestroyBuffer")(context.device, pool.buffer, nullptr); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 40b1730a16..b77564bc69 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,7 +151,7 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The pool fits subgroups in min(2 GiB, storage range); overflow sets a sticky flag checked from `WaitIdle` and present retirement. Header mapped via the first `HOST_VISIBLE|HOST_COHERENT` type. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) and stays until device teardown; a draw that needs more slots sets a sticky flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent, preferring a device-local type when the driver exposes one. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). From 9ef24b1660a8d3fbcf1d118ea916e1810a24e124 Mon Sep 17 00:00:00 2001 From: Maz Date: Fri, 23 Oct 2026 11:26:33 +0400 Subject: [PATCH 281/318] fix(shader): keep the pixel LDS debt note under the length limit --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index b77564bc69..64944d40b1 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,7 +151,7 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) and stays until device teardown; a draw that needs more slots sets a sticky flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent, preferring a device-local type when the driver exposes one. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) until device teardown; excess slots set a sticky flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent, preferring device-local when available. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). From 761af3c8391835468bed365f3bda31aa046e1254 Mon Sep 17 00:00:00 2001 From: Maz Date: Fri, 23 Oct 2026 11:27:23 +0400 Subject: [PATCH 282/318] fix(shader): keep the pixel LDS debt note under the length limit --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 64944d40b1..049ecaee9b 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -151,7 +151,7 @@ - [64-bit LDS atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. On RDNA2 (gfx1036) the LDS serialises a 32-bit atomic on a slot's high dword and a 64-bit atomic on that slot: 128 lanes of `ds_add_u32` and 128 lanes of `ds_add_u64` on one slot sum exactly. Only that pair was measured. Every 32-bit LDS atomic of a program that contains a 64-bit LDS atomic therefore takes the same lock, including a 32-bit `flat_atomic_*` whose address lands in the LDS aperture (a 64-bit one takes it already); the flat forms were not measured against a 64-bit atomic either (the other 32-bit LDS atomics, `ds_min/max`, `ds_cmpst`, the float forms, `ds_inc/dec`, `ds_mskor` and `ds_wrxchg`, are extrapolated from `ds_add_u32` and were not measured against a 64-bit atomic); a program without a 64-bit LDS atomic keeps its native 32-bit atomics. `ds_condxchg32_rtn_b64` is two `ds_mskor` halves, each under the lock but not atomic as a pair, so a 64-bit atomic between the two halves is not excluded. Sub-dword writes (`ds_write_b8`, `ds_write_b16` and their `_d16_hi` forms) and plain `ds_read_*`, `ds_write_*`, `ds_append_u32` and `ds_consume_u32` are not taken under the lock, so a plain access racing a 64-bit atomic on the same dwords can see or overwrite one half of it; whether the hardware orders them was not measured. The lock serialises the workgroup's LDS atomics of such a program; the throughput cost was not measured. `VK_KHR_workgroup_memory_explicit_layout` with `shaderSharedInt64Atomics` would make both widths native and is not used. A 64-bit atomic at an address that is 4-byte but not 8-byte aligned faults the wave on gfx1036, as on gfx1035. When the LDS lives in device memory (compute LDS beyond the device's shared-memory limit) and the device has `shaderBufferInt64Atomics`, no lock is used: a 64-bit atomic is a compare-exchange loop on a 64-bit view of the storage buffer and a 32-bit atomic is a native buffer atomic, a misaligned 64-bit atomic records the `Unaligned` shader fault with the LDS byte address instead of updating, so a mixed-width pair on one slot relies on the device ordering both widths on one buffer address; only `ds_add_u32` against `ds_add_u64` was measured, on gfx1036 (AMD Windows driver 32.0.21042.62) and an RTX 4070 SUPER. The lock is not used there because AMD's Windows driver compiles an acquire or release on a storage buffer, at workgroup scope or wider, to `s_barrier`, and those barriers inside the lock's divergent loop pair with the program's own `s_barrier`s. The fence after each LDS atomic compiles to `s_barrier` on that driver as well, so a wave that skips an LDS atomic that other waves of its workgroup execute misses a barrier they reach; its effect was not measured. On that path a 64-bit atomic at an address that is 4-byte but not 8-byte aligned is skipped and returns 0. - [Image, buffer and global float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`image_atomic_fmin/fmax/fcmpswap`, the same three `buffer_atomic_*` and `global_atomic_*` forms and their `_x2` forms) follow the rule of the LDS float atomics below, including its operand flush: the 32-bit forms flush both operands to a signed zero when bit 0 of the f32 denormal field is clear, the `_x2` forms by the f16/f64 field, and `fcmpswap` stores a denormal source as given. The `flat_atomic_*` forms take the same path and are not measured. Measured on RDNA2 (gfx1031, RX 6700 XT) with the [hardware oracle](../HW_ORACLE.md) over 20 x 20 f32 and 18 x 18 f64 operand pairs in every value of both fields, at IEEE 0 and 1, in wave32 and wave64, and for the image forms on 32_UINT, 32_SINT and 32_FLOAT surfaces. The console's gfx1013 is not measured. - [LDS float atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_min/max/cmpst_f32`, `ds_min/max/cmpst_f64` and their `_rtn` forms) follow the hardware measured on RDNA2 (gfx1036) over 14 x 14 operand sets (zeros, ones, infinities, denormals, quiet and signalling NaNs of both signs and with payloads) in IEEE 0 and 1 with every value 0-3 of the type's denormal field: both operands are flushed to a signed zero when bit 0 of the field is clear (0 and 2; 1 and 3 keep them; the f32 field for the f32 forms, the f16/f64 field for the f64 forms; a shader without a known float mode flushes f32 and keeps f64), then a signalling-NaN data operand is stored quieted, else a signalling-NaN old value is stored quieted, else a quiet-NaN old value is replaced by the data, else a quiet-NaN data operand loses, else the ordered compare with -0 below +0 decides, and the flushed value is what is written, also when the old value wins. `cmpst` compares the flushed bits (both zeros equal, NaN never equal) and leaves memory unchanged when they differ. The returned value is always the old value as it was in memory. `IEEE_MODE` has no effect. Not measured: a denormal or signalling-NaN `data1` of `cmpst` (stored as given). `ds_add_f32` and `ds_add_rtn_f32` follow RDNA1 (gfx1010, RX 5700 XT), measured with the [hardware oracle](../HW_ORACLE.md) template run through the Windows driver's comgr and HIP runtime over 1574 operand pairs (signed zeros, denormals, the smallest normals, infinities, quiet and signalling NaNs with payloads, the largest normals, random pairs near the denormal range and pairs that nearly cancel) in all 16 combinations of the f32 denormal field and the f32 round mode, and at IEEE 1 and DX10 clamp 0: both operands are flushed to a signed zero when bit 0 of the field is clear and the result when bit 1 is clear, the sum is rounded to nearest even whatever the round mode, a NaN data operand is stored quieted, else a NaN old value is stored quieted, `+inf + -inf` stores 0xffc00000, and `IEEE_MODE` and `DX10_CLAMP` have no effect. The sum is the host's `OpFAdd` between those flushes, so where the field keeps f32 denormals the result also depends on the host preserving them, which the module does not request. thuanquang measured the same rule on RDNA2 (gfx1031, RX 6700 XT) in #5492, with the same operand classes and 18 modes in wave32 and wave64; the PS5's gfx1013 is not measured. -- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) until device teardown; excess slots set a sticky flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent, preferring device-local when available. Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. +- [LDS in pixel, vertex-path and fetch programs](../../../core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp) (shader recompiler) - console waves share one LDS region; the host uses a private `Function` array (8192 dwords when unbounded). On NVIDIA proprietary, past about 4 GiB of private memory over every resident invocation that array fails (4090 on 617.42: 5456 ok, 5472 fault at 128×48×32 from `VK_NV_shader_sm_builtins`; 5090: 4096 ok, 6144 timeout). Above `min(4096, 4 GiB / (SMs × warps/SM × subgroup × 4))` it is a storage buffer per lane; other drivers keep `Function`. Only the 4090 sizes are measured. The NVIDIA-only pool fits subgroups in min(2 GiB, storage range) until teardown; excess slots sticky-flag and `WaitIdle` throws (no recycle). Header mapped host-visible/coherent (prefer device-local). Sub-dword writes, 32-bit atomics, `ds_append` and `ds_consume` are non-atomic on the lane's copy; append/consume update only the first active lane. Not measured on hardware. - [V# bases off a DWORD boundary](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - raw accesses use the exact bytes, as measured on gfx1035 (Linux, alignment mode UNALIGNED), which differs from the console's mode DWORD: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a raw `buffer_load_dword` with the V# base 1 byte past a dword, or an aligned base and `voffset` 1, returned the aligned dword (0x44332211, not 0x55443322). `s_buffer_load` reads `(base & ~3) + (offset & ~3)`, as on the console. Formatted accesses off their element's alignment throw. Atomics use the byte address `base + offset`, base's low two bits included: measured on RDNA2 (gfx1031, AMD Windows driver 32.0.21045.5002, HIP) with `buffer_atomic_add`, `_swap` and `_cmpswap`, bases 0 to 3 bytes past a DWORD boundary and offsets 0 to 7, an aligned address updates that DWORD and any other stops the dispatch, which the recompiler reports as an unaligned access fault. Closes when: raw buffer accesses drop the two low address bits as on the console - [Unaligned 32-bit buffer atomics](../../../core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp) (shader recompiler) act on the dword that holds their address, as section 8.1.7 of the [RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) describes for alignment mode DWORD, the mode `SH_MEM_CONFIG.alignment_mode` selects on the console: on a PS5 (13.40, measured 2026-10-19 by a gfx1013 kernel run from inside a retail title) a `buffer_atomic_add` of 1 at byte offset 0x0D incremented the dword at 0x0C and left its neighbours unchanged. A 64-bit buffer atomic off an 8-byte boundary still records an unaligned BDA fault: mode DWORD would align it only to 4 bytes, which a host 64-bit atomic cannot address. When the V# base itself is off a DWORD boundary and the containing dword lies before the first aligned dword the buffer view binds to, that atomic is out of bounds and returns 0 without writing. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). From 913e458369456b7f3708db250b36fd2f7b405baf Mon Sep 17 00:00:00 2001 From: Maz Date: Fri, 23 Oct 2026 16:50:43 +0400 Subject: [PATCH 283/318] fix(shader): deserialize pixel function storage dwords at version 45 The SM-budget threshold writes the storage size after the LDS flag; read it back and keep the fixtures on the fused+pointList trailer. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 8 ++++---- .../ControlFlow/src/RequestSerializer.cpp | 5 ++++- .../tests/RequestSerializerTests.cpp | 20 +++++++++++++------ 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 877cf34683..1228701b44 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4967,7 +4967,7 @@ void verifyPixelRequestSerialization() { minimal.context.pixel->layerExported = true; minimal.context.pixel->ditheredAlphaToCoverage = true; const auto encoded = serializer.Serialize(minimal); - require(requestPrefix(encoded, 8u) == "NVNQQSwAAAA=", "new requests did not use serialization version 44"); + require(requestPrefix(encoded, 8u) == "NVNQQS0AAAA=", "new requests did not use serialization version 45"); const std::array fallbacks{0x40u, 0x1ac4u}; auto withFallbacks = minimal; withFallbacks.context.linkedCallFallbacks = fallbacks; @@ -5085,7 +5085,7 @@ void verifyPixelRequestSerialization() { for (std::size_t bytes = 0; bytes < 45u; ++bytes) { expectFailure([&] { static_cast(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted"); } - for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQS0AAAA="}) { + for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQS4AAAA="}) { expectFailure([&] { static_cast(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); } static constexpr std::string_view version18Request{ @@ -5401,7 +5401,7 @@ void verifyDepthRemapRequests() { const auto plainBytes = decodeBase64(plain); const auto remappedBytes = decodeBase64(remapped); constexpr std::size_t pointTailBytes = 13u; - require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 9u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 9 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 2 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 2 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag and the pixel function LDS flag of a vertex request"); + require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 13u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 13 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 6 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 6 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag, the pixel function LDS flag and the pixel function storage size of a vertex request"); for (const std::uint8_t legacyVersion : {32u, 34u}) { auto legacyBytes = remappedBytes; legacyBytes.resize(legacyBytes.size() - sizeof(std::uint32_t)); @@ -5467,7 +5467,7 @@ void verifyMergedHullConfiguration() { constexpr std::size_t pointTailBytes = 13u; auto legacy = decodeBase64(merged); require(legacy.size() > 54u + pointTailBytes && legacy[4] == 45u, "a merged hull request did not use serialization version 45"); - legacy.resize(legacy.size() - 2u - pointTailBytes); + legacy.resize(legacy.size() - sizeof(std::uint32_t) - 2u - pointTailBytes); legacy.pop_back(); require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration"); legacy.erase(legacy.end() - 52); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index c754c198e6..819b5ebfe9 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -936,7 +936,10 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const result.vertex = vertex; } if (version >= 44u) result.request.target.fusedExactArithmetic = reader.ReadBool(); - if (version >= 45u) result.request.target.largePixelFunctionLdsBuffer = reader.ReadBool(); + if (version >= 45u) { + result.request.target.largePixelFunctionLdsBuffer = reader.ReadBool(); + result.request.target.pixelFunctionStorageDwords = reader.ReadU32(); + } if (version >= 21u && reader.Remaining() != 0u) throw std::runtime_error("RequestSerializer: unexpected trailing data"); return result; } diff --git a/core/shader/recompiler/tests/RequestSerializerTests.cpp b/core/shader/recompiler/tests/RequestSerializerTests.cpp index 7e55864b42..fdf1caea63 100644 --- a/core/shader/recompiler/tests/RequestSerializerTests.cpp +++ b/core/shader/recompiler/tests/RequestSerializerTests.cpp @@ -227,6 +227,7 @@ int main() { AppendVertexTail(upgraded); upgraded.push_back(0u); upgraded.push_back(0u); + upgraded.insert(upgraded.end(), {0u, 0x10u, 0u, 0u}); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); auto truncated = bytes; truncated.pop_back(); @@ -269,6 +270,7 @@ int main() { AppendVertexTail(layered); layered.push_back(0u); layered.push_back(0u); + layered.insert(layered.end(), {0u, 0x10u, 0u, 0u}); Require(serializer.Serialize(layerReplay.request) == EncodeBase64(layered)); } } @@ -296,6 +298,7 @@ int main() { AppendVertexTail(bytes); bytes.push_back(0u); bytes.push_back(0u); + bytes.insert(bytes.end(), {0u, 0x10u, 0u, 0u}); Require(serializer.Serialize(replay.request) == EncodeBase64(bytes)); auto truncated23 = DecodeBase64(encoded23); @@ -428,6 +431,7 @@ int main() { AppendVertexTail(upgraded); upgraded.push_back(0u); upgraded.push_back(0u); + upgraded.insert(upgraded.end(), {0u, 0x10u, 0u, 0u}); Require(serializer.Serialize(request) == EncodeBase64(upgraded)); const auto replay38 = serializer.Deserialize(EncodeBase64(main38)); Require(replay38.request.graphics.has_value() && replay38.request.graphics->tessellation.has_value() == fixture.tessellation); @@ -446,6 +450,7 @@ int main() { auto version45 = DecodeBase64(serializer.Serialize(pointed)); Require(version45.size() > 15u && version45[4] == 45u); auto version44 = version45; + version44.resize(version44.size() - 4u); version44.pop_back(); version44[4] = 44u; auto version43 = version44; @@ -488,6 +493,7 @@ int main() { AppendVertexTail(main25); main25.push_back(0u); main25.push_back(0u); + main25.insert(main25.end(), {0u, 0x10u, 0u, 0u}); Require(serializer.Serialize(replay25.request) == EncodeBase64(main25)); } } @@ -515,7 +521,7 @@ int main() { const auto replay40 = serializer.Deserialize(EncodeBase64(version40)); Require(!replay40.request.target.float32DenormPreserve && replay40.request.target.float32FlushToZero == !preserve); auto future = bytes; - future[4] = 46u; + future[4] = 47u; bool futureRejected = false; try { static_cast(serializer.Deserialize(EncodeBase64(future))); @@ -561,6 +567,7 @@ int main() { const auto order = std::search(version41.begin(), version41.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.end()) + pixel.sampleLocations.size() + 1; Require(version41.end() - order > static_cast(sizeof(pixel.sampleOrder)) && *order == 0x10u); version41.erase(order, order + sizeof(pixel.sampleOrder)); + version41.resize(version41.size() - 4u); version41.pop_back(); version41.pop_back(); version41[4] = 41u; @@ -620,7 +627,7 @@ int main() { for (const std::uint8_t version : {32u, 33u, 34u, 35u, 36u, 37u, 38u}) { pointRequest.target.spannedWaves = spanned; auto legacy = DecodeBase64(serializer.Serialize(pointRequest)); - legacy.resize(legacy.size() - (version >= 36u ? 16u : version >= 35u ? 17u : 22u)); + legacy.resize(legacy.size() - (version >= 36u ? 20u : version >= 35u ? 21u : 26u)); legacy[4] = version; const auto replayLegacy = serializer.Deserialize(EncodeBase64(legacy)); Require(replayLegacy.request.target.spannedWaves == spanned); @@ -632,7 +639,7 @@ int main() { Require(!replayLegacy.request.context.vertex->pointList && replayLegacy.request.context.vertex->pointSize == 1.0f); Require(replayLegacy.request.context.vertex->pointSizeMin == 0.0f && replayLegacy.request.context.vertex->pointSizeMax == 0.0f); const auto upgradedLegacy = DecodeBase64(serializer.Serialize(replayLegacy.request)); - Require(upgradedLegacy[4] == 45u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 16u : version >= 35u ? 17u : 22u)); + Require(upgradedLegacy[4] == 45u && upgradedLegacy.size() == legacy.size() + (version >= 36u ? 20u : version >= 35u ? 21u : 26u)); } } @@ -641,14 +648,14 @@ int main() { const auto sampledPoint = serializer.Deserialize(serializer.Serialize(pointRequest)); Require(sampledPoint.request.context.vertex->pointList && sampledPoint.request.context.pixel->sampleOrder == pixel.sampleOrder); auto legacy42 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy42.resize(legacy42.size() - 15u); + legacy42.resize(legacy42.size() - 19u); legacy42[4] = 42u; const auto replay42 = serializer.Deserialize(EncodeBase64(legacy42)); Require(replay42.request.context.pixel->sampleOrder == pixel.sampleOrder && !replay42.request.context.vertex->pointList && replay42.request.context.vertex->pointSize == 1.0f); pixel.sampleOrder = {}; pointRequest.context.pixel = pixel; auto legacy34 = DecodeBase64(serializer.Serialize(pointRequest)); - legacy34.resize(legacy34.size() - 22u); + legacy34.resize(legacy34.size() - 26u); const auto stencil34 = std::search(legacy34.begin(), legacy34.end(), pixel.sampleLocations.begin(), pixel.sampleLocations.begin() + 4) + pixel.sampleLocations.size(); const auto legacyPixelTail = static_cast(1u + sizeof(pixel.sampleOrder)); Require(legacy34.end() - stencil34 > legacyPixelTail && std::all_of(stencil34, stencil34 + legacyPixelTail, [](std::uint8_t value) { return value == 0u; })); @@ -662,7 +669,8 @@ int main() { request.target.fusedExactArithmetic = fused; request.target.largePixelFunctionLdsBuffer = false; auto bytes = DecodeBase64(serializer.Serialize(request)); - Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 2u] == (fused ? 1u : 0u) && bytes.back() == 0u); + Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused && bytes[bytes.size() - 6u] == (fused ? 1u : 0u) && bytes[bytes.size() - 5u] == 0u); + bytes.resize(bytes.size() - 4u); bytes.pop_back(); bytes[4] = 44u; Require(serializer.Deserialize(EncodeBase64(bytes)).request.target.fusedExactArithmetic == fused); From 066b9196369c405b1e61160f08f8eb10ca0e2575 Mon Sep 17 00:00:00 2001 From: Maz Date: Fri, 23 Oct 2026 19:58:33 +0400 Subject: [PATCH 284/318] fix(shader): fall back from BAR pixel LDS memory Prefer DEVICE_LOCAL|HOST_VISIBLE only when its heap fits the pool; if that allocate fails, retry plain host-visible/coherent so non-ReBAR NVIDIA does not throw OUT_OF_DEVICE_MEMORY. --- .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index b06370fd79..146b038eba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -129,17 +129,29 @@ void PreparePixelFunctionLds(const Context& context, VkCommandBuffer commands, s constexpr auto hostCoherent = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; constexpr auto deviceLocalHost = hostCoherent | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; std::optional deviceLocalType; + std::optional hostOnlyType; for (std::uint32_t i = 0; i < context.memory.memoryTypeCount; ++i) { - if ((requirements.memoryTypeBits & (1u << i)) != 0 && (context.memory.memoryTypes[i].propertyFlags & deviceLocalHost) == deviceLocalHost) { + if ((requirements.memoryTypeBits & (1u << i)) == 0) continue; + const auto flags = context.memory.memoryTypes[i].propertyFlags; + const auto heap = context.memory.memoryTypes[i].heapIndex; + if (!deviceLocalType && (flags & deviceLocalHost) == deviceLocalHost && context.memory.memoryHeaps[heap].size >= requirements.size) { deviceLocalType = i; - break; + } + if (!hostOnlyType && (flags & hostCoherent) == hostCoherent && (flags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) == 0) { + hostOnlyType = i; } } - allocation.memoryTypeIndex = deviceLocalType.value_or(context.MemoryType(requirements.memoryTypeBits, hostCoherent)); - const auto allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &pool.memory); + const auto hostType = hostOnlyType.value_or(context.MemoryType(requirements.memoryTypeBits, hostCoherent)); + allocation.memoryTypeIndex = deviceLocalType.value_or(hostType); + auto allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &pool.memory); + if (allocated != VK_SUCCESS && deviceLocalType && allocation.memoryTypeIndex == *deviceLocalType && hostType != *deviceLocalType) { + allocation.memoryTypeIndex = hostType; + allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &pool.memory); + } if (allocated != VK_SUCCESS) { context.Function("vkDestroyBuffer")(context.device, pool.buffer, nullptr); pool.buffer = VK_NULL_HANDLE; + pool.memory = VK_NULL_HANDLE; Check(allocated, "vkAllocateMemory pixel function LDS"); } Check(context.Function("vkBindBufferMemory")(context.device, pool.buffer, pool.memory, 0), "vkBindBufferMemory pixel function LDS"); From 0a3a3bb2687b066d90311b5fba917d00e73a49a4 Mon Sep 17 00:00:00 2001 From: Maz Date: Fri, 23 Oct 2026 20:47:54 +0400 Subject: [PATCH 285/318] fix(shader): strip the pixel LDS bool from legacy fixtures Version 45 adds a one-byte large-pixel LDS flag ahead of the storage dword; older fixture truncations left that byte as unexpected trailing data on Windows. --- .../prx/libSceAgcDriver/tests/ShaderMemory.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 1228701b44..eb2a9c2e28 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5243,12 +5243,13 @@ void verifyVertexInfoVersion19() { orderedVersion34.pop_back(); orderedVersion34.pop_back(); orderedVersion34.pop_back(); + orderedVersion34.pop_back(); orderedVersion34.erase(orderedVersion34.begin() + static_cast(orderOffset - 1u), orderedVersion34.begin() + static_cast(orderOffset + sampleOrderBytes)); orderedVersion34[4] = 34u; const auto orderedReplay34 = serializer.Deserialize(encodeBase64(orderedVersion34)); require(orderedReplay34.request.context.pixel.has_value() && orderedReplay34.request.context.pixel->sampleOrder == std::array{} && serializer.Serialize(orderedReplay34.request) == encodedPixel, "a version 34 request read a sample order, or misread the fields after its pixel iteration fields"); auto orderedVersion30 = orderedBytes; - orderedVersion30.resize(orderedVersion30.size() - 5u - sizeof(std::uint32_t)); + orderedVersion30.resize(orderedVersion30.size() - 6u - sizeof(std::uint32_t)); orderedVersion30.erase(orderedVersion30.begin() + static_cast(blendOffset + 4u), orderedVersion30.begin() + static_cast(orderOffset + sampleOrderBytes)); orderedVersion30[4] = 30u; const auto orderedReplay30 = serializer.Deserialize(encodeBase64(orderedVersion30)); @@ -5259,6 +5260,7 @@ void verifyVertexInfoVersion19() { orderedVersion32.resize(orderedVersion32.size() - sizeof(std::uint32_t)); orderedVersion32.pop_back(); orderedVersion32.pop_back(); + orderedVersion32.pop_back(); require(orderedVersion32.size() > 2u && orderedVersion32.back() == 0u && orderedVersion32[orderedVersion32.size() - 2u] == 0u && orderedVersion32[orderedVersion32.size() - 3u] == 1u, "the spanned-waves flag is not the byte before the depth remap flag"); orderedVersion32.pop_back(); orderedVersion32.pop_back(); @@ -5285,6 +5287,7 @@ void verifyVertexInfoVersion19() { version30.pop_back(); version30.pop_back(); version30.pop_back(); + version30.pop_back(); version30.erase(version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u), version30.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version30[4] = 30u; const auto replay30 = serializer.Deserialize(encodeBase64(version30)); @@ -5310,11 +5313,13 @@ void verifyVertexInfoVersion19() { version29.pop_back(); version29.pop_back(); version29.pop_back(); + version29.pop_back(); version29.erase(version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes), version29.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); auto version36 = sampledBytes; version36.resize(version36.size() - sizeof(std::uint32_t)); version36.pop_back(); version36.pop_back(); + version36.pop_back(); version36.erase(version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes - 1u), version36.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version36[4] = 36u; const auto replay36 = serializer.Deserialize(encodeBase64(version36)); @@ -5329,6 +5334,7 @@ void verifyVertexInfoVersion19() { version28.pop_back(); version28.pop_back(); version28.pop_back(); + version28.pop_back(); version28.erase(version28.begin() + static_cast(maskOffset + 1u), version28.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version28[4] = 28u; const auto replay28 = serializer.Deserialize(encodeBase64(version28)); @@ -5340,6 +5346,7 @@ void verifyVertexInfoVersion19() { version27.pop_back(); version27.pop_back(); version27.pop_back(); + version27.pop_back(); version27.erase(version27.begin() + static_cast(maskOffset), version27.begin() + static_cast(maskOffset + 1u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); version27[4] = 27u; const auto replay27 = serializer.Deserialize(encodeBase64(version27)); @@ -5351,6 +5358,7 @@ void verifyVertexInfoVersion19() { legacyPixel.pop_back(); legacyPixel.pop_back(); legacyPixel.pop_back(); + legacyPixel.pop_back(); legacyPixel.erase(legacyPixel.begin() + static_cast(allBitsOffset), legacyPixel.begin() + static_cast(allBitsOffset + 2u + sampleMaskBytes + 4u + iterationBytes + sampleOrderBytes)); legacyPixel[4] = 26u; const auto replay26 = serializer.Deserialize(encodeBase64(legacyPixel)); @@ -5379,9 +5387,11 @@ void verifyVertexInfoVersion19() { require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported && !replayPixel.request.context.pixel->ditheredAlphaToCoverage && replayPixel.request.context.pixel->reversedBlend == 0u, (what + " lost its pixel primitive class or read a layer export, dither flag or reversed-order blend code").c_str()); require(serializer.Serialize(replayPixel.request) == encodedPixel, (what + " misread the fields after its pixel primitive class").c_str()); } + std::cerr << "exit verifyVertexInfoVersion19\n"; } void verifyDepthRemapRequests() { + std::cerr << "enter verifyDepthRemapRequests\n"; using namespace ShaderRecompiler; const std::array code{0xbf810000u}; const RequestSerializer serializer; @@ -5394,6 +5404,7 @@ void verifyDepthRemapRequests() { vertex.target.depthRangeRemap = true; vertex.context.vertex->depthRemapMode = 2u; const auto remapped = serializer.Serialize(vertex); + std::cerr << "DR roundtrip\n"; const auto replay = serializer.Deserialize(remapped); require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip"); require(replay.request.context.vertex->depthRemapMode == 2u, "the depth remap mode did not round-trip"); @@ -5408,8 +5419,10 @@ void verifyDepthRemapRequests() { legacyBytes.pop_back(); legacyBytes.pop_back(); legacyBytes.pop_back(); + legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes); + std::cerr << "DR legacy\n"; const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u, ("a version " + std::to_string(legacyVersion) + " vertex request read depth remap fields").c_str()); require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); @@ -5421,6 +5434,7 @@ void verifyDepthRemapRequests() { version35.pop_back(); version35[4] = 35u; version35.resize(version35.size() - 1u - pointTailBytes); + std::cerr << "DR replay35\n"; const auto replay35 = serializer.Deserialize(encodeBase64(version35)); require(replay35.request.target.depthRangeRemap && replay35.request.context.vertex->depthRemapMode == 2u && !replay35.request.target.float32FlushToZero && serializer.Serialize(replay35.request) == remapped, "a version 35 request misread the depth remap fields or read an f32 flush flag"); auto truncated = version35; From 500d77aef31971df7de892e4bd2ee8d911ae954f Mon Sep 17 00:00:00 2001 From: Maz Date: Fri, 23 Oct 2026 20:51:20 +0400 Subject: [PATCH 286/318] fix(shader): drop debug traces from pixel LDS fixture strip Keep the version-45 bool pops for the pixel legacy fixtures; revert the extra depth-remap legacyBytes pop that truncated those heads, and remove temporary cerr probes. --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index eb2a9c2e28..b335495fc4 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -5387,11 +5387,9 @@ void verifyVertexInfoVersion19() { require(replayPixel.request.context.pixel.has_value() && replayPixel.request.context.pixel->rasterPrimitive == RasterPrimitive::Rectangle && !replayPixel.request.context.pixel->layerExported && !replayPixel.request.context.pixel->ditheredAlphaToCoverage && replayPixel.request.context.pixel->reversedBlend == 0u, (what + " lost its pixel primitive class or read a layer export, dither flag or reversed-order blend code").c_str()); require(serializer.Serialize(replayPixel.request) == encodedPixel, (what + " misread the fields after its pixel primitive class").c_str()); } - std::cerr << "exit verifyVertexInfoVersion19\n"; } void verifyDepthRemapRequests() { - std::cerr << "enter verifyDepthRemapRequests\n"; using namespace ShaderRecompiler; const std::array code{0xbf810000u}; const RequestSerializer serializer; @@ -5404,7 +5402,6 @@ void verifyDepthRemapRequests() { vertex.target.depthRangeRemap = true; vertex.context.vertex->depthRemapMode = 2u; const auto remapped = serializer.Serialize(vertex); - std::cerr << "DR roundtrip\n"; const auto replay = serializer.Deserialize(remapped); require(replay.request.target.depthRangeRemap && replay.request.context.vertex.has_value() && replay.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "the depth remap target flag or the vertex info did not round-trip"); require(replay.request.context.vertex->depthRemapMode == 2u, "the depth remap mode did not round-trip"); @@ -5419,10 +5416,8 @@ void verifyDepthRemapRequests() { legacyBytes.pop_back(); legacyBytes.pop_back(); legacyBytes.pop_back(); - legacyBytes.pop_back(); legacyBytes[4] = legacyVersion; legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes); - std::cerr << "DR legacy\n"; const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes)); require(!legacy.request.target.depthRangeRemap && legacy.request.context.vertex.has_value() && legacy.request.context.vertex->depthRemapMode == 0u, ("a version " + std::to_string(legacyVersion) + " vertex request read depth remap fields").c_str()); require(serializer.Serialize(legacy.request) == plain, ("a version " + std::to_string(legacyVersion) + " vertex request misread the fields before the depth remap").c_str()); @@ -5434,7 +5429,6 @@ void verifyDepthRemapRequests() { version35.pop_back(); version35[4] = 35u; version35.resize(version35.size() - 1u - pointTailBytes); - std::cerr << "DR replay35\n"; const auto replay35 = serializer.Deserialize(encodeBase64(version35)); require(replay35.request.target.depthRangeRemap && replay35.request.context.vertex->depthRemapMode == 2u && !replay35.request.target.float32FlushToZero && serializer.Serialize(replay35.request) == remapped, "a version 35 request misread the depth remap fields or read an f32 flush flag"); auto truncated = version35; From a0ce5788b30b3c221a7ed8be3c60beaddbd70532 Mon Sep 17 00:00:00 2001 From: Luka Petrovic Date: Fri, 23 Oct 2026 19:37:01 +0200 Subject: [PATCH 287/318] fix(tools): validate compilation database entries --- tools/check_compile.py | 41 +++++++++++++++++++++--- tools/tests/test_check_compile.py | 53 ++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/tools/check_compile.py b/tools/check_compile.py index 9b66a1cd16..c2c0befc3a 100644 --- a/tools/check_compile.py +++ b/tools/check_compile.py @@ -40,19 +40,53 @@ def repository(): return Path(result.stdout.strip()) +def validate_arguments(arguments): + if not isinstance(arguments, list) or not arguments or any(not isinstance(argument, str) for argument in arguments) or not arguments[0]: + raise ValueError("arguments must be a nonempty list of strings with an executable") + + +def compilation_database(text, source, skip_empty_commands=False): + database = json.loads(text) + if not isinstance(database, list): + raise ValueError(f"{source}: expected an array of command objects") + commands = [] + for index, entry in enumerate(database): + context = f"{source}: entry {index}" + if not isinstance(entry, dict): + raise ValueError(f"{context}: expected a command object") + for field in ("directory", "file"): + if field not in entry: + raise ValueError(f"{context}: missing {field}") + if not isinstance(entry[field], str) or not entry[field]: + raise ValueError(f"{context}: {field} must be a nonempty string") + if skip_empty_commands and "arguments" not in entry and entry.get("command") == "": + continue + if "arguments" in entry: + try: + validate_arguments(entry["arguments"]) + except ValueError as error: + raise ValueError(f"{context}: {error}") from error + elif "command" not in entry: + raise ValueError(f"{context}: expected arguments or command") + elif not isinstance(entry["command"], str) or not entry["command"].strip(): + raise ValueError(f"{context}: command must be a nonempty string") + commands.append(entry) + return commands + + def entries(build): if not build.is_dir(): raise ValueError(f"{build} is not a directory; configure the build first") manifest = build / "compile_commands.json" if manifest.is_file(): - return json.loads(manifest.read_text(encoding="utf-8-sig")) + return compilation_database(manifest.read_text(encoding="utf-8-sig"), manifest) ninja = shutil.which("ninja") if ninja is None: raise ValueError(f"no {manifest} and ninja is not on PATH") result = capture([ninja, "-C", str(build), "-t", "compdb", "-x"], encoding=ninja_encoding(ninja)) if result.returncode != 0: raise ValueError(f"ninja -t compdb: {result.stderr.strip()}") - return json.loads(result.stdout) + return compilation_database(result.stdout, "ninja -t compdb", skip_empty_commands=True) def entry_key(entry, root): @@ -116,8 +150,7 @@ def syntax_only(entry, source): def syntax_arguments(arguments, directory, source): - if not isinstance(arguments, list) or not arguments or any(not isinstance(argument, str) for argument in arguments) or not arguments[0]: - raise ValueError("arguments must be a nonempty list of strings with an executable") + validate_arguments(arguments) for argument in arguments[1:]: if argument.startswith("@"): raise ValueError(f"unexpanded response file {argument[1:]}; provide expanded compile arguments before checking syntax") diff --git a/tools/tests/test_check_compile.py b/tools/tests/test_check_compile.py index ffd20390ab..c2f6f77643 100644 --- a/tools/tests/test_check_compile.py +++ b/tools/tests/test_check_compile.py @@ -88,7 +88,7 @@ class CheckCompileTests(unittest.TestCase): (self.build / "build.ninja").write_text( "rule cc\n command = " + command + " @$out.rsp\n" " rspfile = $out.rsp\n rspfile_content = -DREQUIRED -c $in -o $out -MD -MF $out.d\n" - "build unit.o: cc ../unit.c\n", encoding="utf-8") + "build order: phony\nbuild unit.o: cc ../unit.c || order\n", encoding="utf-8") self.assertFalse((self.build / "unit.o.rsp").exists()) result = self.tool("unit.c") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) @@ -515,6 +515,57 @@ class CheckCompileTests(unittest.TestCase): self.assertIn("arguments must be a nonempty list of strings with an executable", result.stderr) self.assertNotIn("Traceback", result.stderr) + def test_invalid_compilation_databases_are_rejected_without_a_traceback(self): + for database in ({}, {"file": "unit.cpp"}, None, 42, "commands"): + with self.subTest(database=database): + self.manifest(database) + result = self.tool("unit.cpp") + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn("compile_commands.json: expected an array of command objects", result.stderr) + self.assertNotIn("Traceback", result.stderr) + + def test_invalid_entries_are_rejected_before_any_compiler_runs(self): + source = self.source("unit.cpp") + invoked = self.root / "invoked" + script = self.compiler("import sys\nfrom pathlib import Path\nPath(sys.argv[1]).touch()\n") + valid = self.entry("unit.cpp", f'"{sys.executable}" "{script}" "{invoked}" "{source}"') + cases = [(value, "expected a command object") for value in (None, [], 42, "command")] + for field in ("directory", "file"): + missing = dict(valid) + del missing[field] + cases.append((missing, f"missing {field}")) + cases.extend(({**valid, field: value}, f"{field} must be a nonempty string") + for value in (None, [], 42, "")) + cases.append(({key: value for key, value in valid.items() if key != "command"}, + "expected arguments or command")) + cases.extend(({**valid, "command": value}, "command must be a nonempty string") + for value in (None, [], 42, "", " \t")) + cases.append(({**valid, "arguments": []}, + "arguments must be a nonempty list of strings with an executable")) + for invalid, message in cases: + with self.subTest(invalid=invalid): + self.manifest([valid, invalid]) + result = self.tool("--jobs", "2", "unit.cpp") + self.assertEqual(result.returncode, 2, result.stdout + result.stderr) + self.assertIn(f"compile_commands.json: entry 1: {message}", result.stderr) + self.assertNotIn("Traceback", result.stderr) + self.assertFalse(invoked.exists(), result.stdout + result.stderr) + + def test_invalid_ninja_databases_are_validated_before_compilation(self): + source = self.source("unit.cpp") + cases = (({}, "expected an array of command objects"), + ([self.entry("unit.cpp", "compiler"), None], "entry 1: expected a command object")) + for database, message in cases: + with self.subTest(database=database), \ + patch.object(check_compile.shutil, "which", return_value="ninja"), \ + patch.object(check_compile, "ninja_encoding", return_value="utf-8"), \ + patch.object(check_compile, "capture", return_value=subprocess.CompletedProcess( + [], 0, json.dumps(database), "")), \ + patch.object(check_compile.subprocess, "run") as compiler: + with self.assertRaisesRegex(ValueError, "ninja -t compdb: " + message): + check_compile.check(self.root, self.build, [source]) + compiler.assert_not_called() + def test_parallel_checks_overlap_but_diagnostics_keep_source_order(self): barrier = self.root / "barrier" barrier.mkdir() From 7333de6aee8049133cb8bc47c3ddd7d44809a306 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 21 Oct 2026 10:57:34 +0000 Subject: [PATCH 288/318] fix(agc): give a depth stencil copy's layout barrier both aspects The depth sample reorder copies a D32S8 surface into an image of the same format. When only depth is reordered, the copy's transition to GENERAL named the depth aspect alone, which Vulkan does not allow for a depth/stencil format without separateDepthStencilLayouts (VUID-VkImageMemoryBarrier-image-03320). The barrier now names both aspects of a format with stencil, as transitionLocated does; the copy itself still moves depth only. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp index dfcef91efc..647b38274c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/SampleOrder.cpp @@ -185,6 +185,7 @@ public: auto copy = copyFor(target.format, target.samples, target.extent, stencil); kept.push_back(copy); const VkImageAspectFlags aspects = VK_IMAGE_ASPECT_DEPTH_BIT | (stencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); + const VkImageAspectFlags layoutAspects = VK_IMAGE_ASPECT_DEPTH_BIT | (HasStencil(target.format) ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u); if (!copy->general) { VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -193,7 +194,7 @@ public: toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toGeneral.image = copy->image; - toGeneral.subresourceRange = {aspects, 0, 1, 0, 1}; + toGeneral.subresourceRange = {layoutAspects, 0, 1, 0, 1}; context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral); copy->general = true; } From b0b7198b7a0ca2673deab11c4a87f64fae871e8b Mon Sep 17 00:00:00 2001 From: Benjamin Cortez Date: Wed, 7 Oct 2026 21:35:14 -0300 Subject: [PATCH 289/318] feat(libSceAudioOut): implement sceAudioOutGetSystemState --- core/libs/SceTypes.hpp | 7 ++++ core/libs/prx/libSceAudioOut/CMakeLists.txt | 10 +++++ core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 8 ++++ .../tests/AudioOutSystemState.cpp | 39 +++++++++++++++++++ docs/dev/debt/libSceAudioOut.md | 1 + 5 files changed, 65 insertions(+) create mode 100644 core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 4a61c38ed7..f51297c09b 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -655,6 +655,13 @@ struct AudioOutPortState { std::uint64_t activeState; }; +struct AudioOutSystemState { + float loudness; + std::uint8_t reserved8[4]; + std::uint64_t reserved[3]; +}; +static_assert(sizeof(AudioOutSystemState) == 32); + struct Audio3dOpenParameters { std::uint64_t size_this; std::uint32_t granularity; diff --git a/core/libs/prx/libSceAudioOut/CMakeLists.txt b/core/libs/prx/libSceAudioOut/CMakeLists.txt index ba5e91090b..7c97d07a55 100644 --- a/core/libs/prx/libSceAudioOut/CMakeLists.txt +++ b/core/libs/prx/libSceAudioOut/CMakeLists.txt @@ -245,4 +245,14 @@ if(BUILD_TESTING) set_tests_properties(audio_out2_shutdown audio_out2_waiting_push_shutdown audio_out2_advance_shutdown audio_out_shutdown PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(audio_out_system_state_tests tests/AudioOutSystemState.cpp) + target_include_directories(audio_out_system_state_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(audio_out_system_state_tests PRIVATE libSceAudioOut libc) + configure_windows_unwind(audio_out_system_state_tests) + add_test(NAME audio_out_system_state COMMAND audio_out_system_state_tests) + if(WIN32) + set_tests_properties(audio_out_system_state PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 9faeaef957..8f1573f958 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -714,4 +714,12 @@ int APS5_VABI sceAudioOutExOpen(int userId, std::uint32_t mode) { return allocatePort(PORT_TYPE_EX, highRate ? 1024 : 256, highRate ? 192000 : 48000, Format::S16Stereo); } +int APS5_VABI sceAudioOutGetSystemState(AudioOutSystemState* state) { + if (state == nullptr) { + return -2144993276; + } + state->loudness = 0.0f; + return 0; +} + } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp new file mode 100644 index 0000000000..4cf928d770 --- /dev/null +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp @@ -0,0 +1,39 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include + +extern "C" { +int APS5_VABI sceAudioOutGetSystemState(AudioOutSystemState*); +} + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +namespace { + +constexpr int invalidPointer = static_cast(0x80260004); + +void TestGetSystemState() { + AudioOutSystemState state{}; + state.loudness = -1.0f; + Require(sceAudioOutGetSystemState(&state) == 0, "sceAudioOutGetSystemState must succeed with valid pointer"); + Require(state.loudness == 0.0f, "loudness must be reported as 0.0f"); +} + +void TestInvalidPointer() { + Require(sceAudioOutGetSystemState(nullptr) == invalidPointer, "null pointer must return SCE_AUDIO_OUT_ERROR_INVALID_POINTER"); +} + +} + +int main() { + TestGetSystemState(); + TestInvalidPointer(); + return 0; +} diff --git a/docs/dev/debt/libSceAudioOut.md b/docs/dev/debt/libSceAudioOut.md index 37e4a53c5a..83e83145fa 100644 --- a/docs/dev/debt/libSceAudioOut.md +++ b/docs/dev/debt/libSceAudioOut.md @@ -18,6 +18,7 @@ - [sceAudioOut2GetSpeakerArrayCoefficients](../../../core/libs/prx/libSceAudioOut/src/AudioOut2SpeakerArray.cpp) (libSceAudioOut) - validates the handle and the coefficient count like the ambisonics call, then writes the front pair at full gain and silence for every other speaker, ignoring the position, spread, height awareness and downmix spread radius: [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libAudio2.cpp) and [prosper](https://github.com/mattias800/prosper/blob/main/prosper/src/hle/audio/hle_audio.cpp) both return that pair. The console's VBAP gains over the array's directions are not measured - [sceAudioOutSetMixLevelPadSpk](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function - [sceAudioOutGetLastOutputTime](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device +- [sceAudioOutGetSystemState](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - the 32-byte layout (`float loudness`, 4 reserved bytes, 3 reserved 64-bit words) follows shadPS4's `OrbisAudioOutSystemState` and SharpProspero's `SceAudioOutSystemState` (`Size = 32`); it is not confirmed on a console. Loudness is reported as a constant 0.0f, although SharpProspero documents it in LKFS, so the value is unconfirmed. shadPS4 returns NOT_INIT before `sceAudioOutInit`; that check is not modelled. Imported by PPSA30528, which fails to load without it - [sceAudioOutSysOpen, sceAudioOutSysConfigureOutput, sceAudioOutSysClose](../../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - imported by PPSA99051 and PPSA99997, which fail to load without them; the signatures follow [EVO-PLAYER-PS5](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/evo_pt.cpp#L24-L29), whose audio code both titles share, and are not confirmed on a console. `sceAudioOutSysOpen(user, mode)` opens a port only for mode 5, the TrueHD MAT route: [the same project's carrier table](https://github.com/sainsaji/EVO-PLAYER-PS5/blob/da0c0451333bab82e79fffd2de4718e917f9dcb0/projects/evoplayer/core/src/audio/iec61937.cpp#L125-L140), read from sceAudioOutExOpen's own tables in firmware 12.70, describes it as a 768 kHz port of 1024 S16 8-channel frames whose 16-byte frames are consumed at 192 kHz; every other mode throws. Audio reaches the host through SDL as PCM and the host has no HDMI bitstream output, so like an Ex port a Sys port has no host device: `sceAudioOutOutput` paces it at its own rate and drops the MAT bursts. `sceAudioOutSysConfigureOutput(type, flags, mode, target, option)` accepts only the observed `(1, 0, 5, 1, 0)` HDMI MAT switch and changes nothing; other arguments throw. `sceAudioOutSysClose` closes a Sys port and throws for any other handle. The restore path EVO-PLAYER-PS5 takes after a Sys port calls `sceAudioOutExConfigureOutput(0, 0, 0xFF, 0xFF, 0)`, so no Sys reset combination is known - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - MAIN (type 0) format validation follows PS5 firmware 13.60 measurements: bits 8-11 accept 1, 2, 8 or 12, bits 0-6 accept 0 or 1, bit 7 requires 8 channels, and bits 12-31 do not affect validation. The sample types are still read as float and int16; bit 7's layout, the PCM effect of bits 12-31 and the port `flags` are unverified. Other port types retain unverified format validation, including 6 channels and rejection of bits 12-31 - [sceAudioOut2PortCreate](../../../core/libs/prx/libSceAudioOut/src/AudioOut2Port.cpp) (libSceAudioOut) - 12 channels are assumed to be L R C LFE Ls Rs Lb Rb Ltf Rtf Ltb Rtb as in KytyPS5's 12-to-8 fold, and 6 and 8 channels to be its first 6 and 8; no title or SDK header confirms the order From de28243d263e0d00a36d231b41c18177b6e240e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 23 Oct 2026 20:18:56 +0200 Subject: [PATCH 290/318] fix(libSceAudioOut): clear sceAudioOutGetSystemState reserved fields Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAudioOut/src/AudioOut.cpp | 1 + core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp index 8f1573f958..e973018f35 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut.cpp @@ -718,6 +718,7 @@ int APS5_VABI sceAudioOutGetSystemState(AudioOutSystemState* state) { if (state == nullptr) { return -2144993276; } + *state = {}; state->loudness = 0.0f; return 0; } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp index 4cf928d770..614a2ba2eb 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOutSystemState.cpp @@ -22,8 +22,11 @@ constexpr int invalidPointer = static_cast(0x80260004); void TestGetSystemState() { AudioOutSystemState state{}; state.loudness = -1.0f; + state.reserved8[0] = 0xAA; + state.reserved[2] = ~0ull; Require(sceAudioOutGetSystemState(&state) == 0, "sceAudioOutGetSystemState must succeed with valid pointer"); Require(state.loudness == 0.0f, "loudness must be reported as 0.0f"); + Require(state.reserved8[0] == 0 && state.reserved[2] == 0, "reserved fields must be cleared"); } void TestInvalidPointer() { From c345ed42d20242092625b3192d048e2c63eb1acd Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:09:36 +0100 Subject: [PATCH 291/318] feat(libSceCesCs): add the strict JIS X 0208 EUC-JP and Shift-JIS profiles Implement sceCesUcsProfileInitEucJpX0208, sceCesUcsProfileInitEucJpX0208Ss2, sceCesUcsProfileInitEucJpX0208Ss2Ss3 and sceCesUcsProfileInitSJis1997X0208. The JIS X 0208 table is JIS0208.TXT, derived from the CP932 table without rows 13 and 89-92 and with six cells changed. The EUC-JP variants add SS2 half-width katakana and SS3 JIS X 0212 as their names say. Cell 0x2140 and the Shift-JIS single bytes 0x5C and 0x7E, where the sources disagree, throw. --- core/libs/prx/libSceCesCs/CMakeLists.txt | 1 + core/libs/prx/libSceCesCs/Export.cpp | 148 +++++++++-- core/libs/prx/libSceCesCs/PendingExports.cpp | 20 -- core/libs/tests/GuestCesJapanese.cpp | 250 ++++++++++++++++++- docs/dev/debt/libSceCesCs.md | 3 +- 5 files changed, 372 insertions(+), 50 deletions(-) diff --git a/core/libs/prx/libSceCesCs/CMakeLists.txt b/core/libs/prx/libSceCesCs/CMakeLists.txt index c59a762ef5..991387c540 100644 --- a/core/libs/prx/libSceCesCs/CMakeLists.txt +++ b/core/libs/prx/libSceCesCs/CMakeLists.txt @@ -38,6 +38,7 @@ if(BUILD_TESTING) add_test_executable(guest_ces_${variant}_tests ${LIBS_INCLUDE_DIR}/tests/GuestCesUnicode.cpp) endif() target_include_directories(guest_ces_${variant}_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_windows_unwind(guest_ces_${variant}_tests) if(variant MATCHES "_module$") target_link_libraries(guest_ces_${variant}_tests PRIVATE libSceCesCs-module) else() diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 8ebedd1665..46e3aad1fe 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -147,14 +148,74 @@ uint32_t encodeUtf8(uint32_t code, uint8_t* utf8) { } constexpr uint8_t EUC_JP_TAG[8] = {'E', 'U', 'C', 'J', 'P', '9', '3', '2'}; +constexpr uint8_t EUC_JP_X0208_TAG[8] = {'E', 'U', 'C', 'J', 'X', '2', '0', '8'}; +constexpr uint8_t EUC_JP_X0208_SS2_TAG[8] = {'E', 'U', 'C', 'J', 'X', 'S', 'S', '2'}; +constexpr uint8_t EUC_JP_X0208_SS3_TAG[8] = {'E', 'U', 'C', 'J', 'X', 'S', 'S', '3'}; constexpr uint8_t SJIS_TAG[8] = {'S', 'J', 'I', 'S', '1', '9', '9', '7'}; +constexpr uint8_t SJIS_X0208_TAG[8] = {'S', 'J', 'I', 'S', 'X', '2', '0', '8'}; + +bool hasTag(const void* profile, const uint8_t (&tag)[8]) { + return profile != nullptr && std::memcmp(profile, tag, sizeof(tag)) == 0; +} + +bool isEucJpX0208(const void* profile) { + return hasTag(profile, EUC_JP_X0208_TAG) || hasTag(profile, EUC_JP_X0208_SS2_TAG) || hasTag(profile, EUC_JP_X0208_SS3_TAG); +} bool isEucJp(const void* profile) { - return profile != nullptr && std::memcmp(profile, EUC_JP_TAG, sizeof(EUC_JP_TAG)) == 0; + return hasTag(profile, EUC_JP_TAG) || isEucJpX0208(profile); +} + +bool hasSs2(const void* profile) { + return !hasTag(profile, EUC_JP_X0208_TAG); +} + +bool hasSs3(const void* profile) { + return hasTag(profile, EUC_JP_TAG) || hasTag(profile, EUC_JP_X0208_SS3_TAG); +} + +bool isSjisX0208(const void* profile) { + return hasTag(profile, SJIS_X0208_TAG); } bool isSjis(const void* profile) { - return profile != nullptr && std::memcmp(profile, SJIS_TAG, sizeof(SJIS_TAG)) == 0; + return hasTag(profile, SJIS_TAG) || isSjisX0208(profile); +} + +struct JisChange { + uint16_t jis; + uint16_t code; +}; + +constexpr JisChange JIS_X0208_CHANGES[6] = {{0x2141, 0x301C}, {0x2142, 0x2016}, {0x215D, 0x2212}, {0x2171, 0x00A2}, {0x2172, 0x00A3}, {0x224C, 0x00AC}}; +constexpr uint16_t JIS_X0208_UNSETTLED_CELL = 0x2140; +constexpr uint16_t JIS_X0208_UNSETTLED_CODE = 0xFF3C; +constexpr const char* JIS_X0208_UNSETTLED = "libSceCesCs: the console's mapping of JIS X 0208 cell 0x2140 (U+005C or U+FF3C) is unknown"; + +constexpr uint32_t jisIndex(uint16_t jis) { + return ((jis >> 8) - 0x21) * 94 + (jis & 0xFF) - 0x21; +} + +bool isCp932ExtensionRow(uint32_t row) { + return row == 13 || (row >= 89 && row <= 92); +} + +uint16_t strictJisX0208(uint32_t index) { + if (isCp932ExtensionRow(index / 94 + 1) || index == jisIndex(JIS_X0208_UNSETTLED_CELL)) return 0; + for (const JisChange& change : JIS_X0208_CHANGES) { + if (jisIndex(change.jis) == index) return change.code; + } + return JisX0208::TO_UCS2[index]; +} + +uint32_t jisX0208ToUnicode(bool strict, uint32_t index) { + if (!strict) return JisX0208::TO_UCS2[index]; + if (index == jisIndex(JIS_X0208_UNSETTLED_CELL)) throw std::runtime_error(JIS_X0208_UNSETTLED); + return strictJisX0208(index); +} + +void requireSettledSjisX0208(uint32_t code) { + if (code == 0x5C || code == 0x7E || code == 0xA5 || code == 0x203E) throw std::runtime_error("libSceCesCs: the console's mapping of Shift-JIS bytes 0x5C and 0x7E (ASCII or JIS X 0201 Roman) is unknown"); } constexpr uint8_t EUC_CN_TAG[8] = {'E', 'U', 'C', 'C', 'N', '9', '3', '6'}; @@ -391,7 +452,9 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 return 0; } if (isSjis(profile)) { + const bool strict = isSjisX0208(profile); if (lead < 0x80) { + if (strict) requireSettledSjisX0208(lead); length = 1; code = lead; return 0; @@ -405,7 +468,7 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; const uint8_t trail = mbcs[1]; if (trail < 0x40 || trail == 0x7F || trail > 0xFC) return CES_ERROR_INVALID_ENCODE; - if (lead >= 0xFA) { + if (lead >= 0xFA && !strict) { const uint32_t trailIdx = trail < 0x7F ? trail - 0x40 : trail - 0x41; const uint32_t offset = (lead - 0xFA) * 188 + trailIdx; if (offset <= 387) { @@ -428,7 +491,7 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 int cell = 0; sjisToJis(lead, trail, row, cell); if (row < 1 || row > 94 || cell < 1 || cell > 94) return CES_ERROR_UNASSIGNED_CODE; - code = JisX0208::TO_UCS2[(row - 1) * 94 + (cell - 1)]; + code = jisX0208ToUnicode(strict, (row - 1) * 94 + (cell - 1)); if (code == 0) return CES_ERROR_UNASSIGNED_CODE; length = 2; return 0; @@ -440,7 +503,7 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 code = lead; return 0; } - if (lead == 0x8E) { + if (lead == 0x8E && hasSs2(profile)) { if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; if (mbcs[1] < 0xA1 || mbcs[1] > 0xDF) return CES_ERROR_INVALID_ENCODE; length = 2; @@ -451,10 +514,10 @@ int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint3 if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE) return CES_ERROR_INVALID_ENCODE; length = 2; - code = JisX0208::TO_UCS2[(lead - 0xA1) * 94 + (mbcs[1] - 0xA1)]; + code = jisX0208ToUnicode(isEucJpX0208(profile), (lead - 0xA1) * 94 + (mbcs[1] - 0xA1)); return code != 0 ? 0 : CES_ERROR_UNASSIGNED_CODE; } - if (lead == 0x8F) { + if (lead == 0x8F && hasSs3(profile)) { if (mbcsMax < 3) return CES_ERROR_SRC_BUFFER_END; if (mbcs[1] < 0xA1 || mbcs[1] > 0xFE || mbcs[2] < 0xA1 || mbcs[2] > 0xFE) return CES_ERROR_INVALID_ENCODE; length = 3; @@ -470,20 +533,27 @@ struct JisCell { bool supplementary; }; -const std::vector& jisReverse() { - static const auto reverse = [] { - std::vector entries; - entries.reserve(2 * 94 * 94); - for (uint32_t cell = 0; cell < 94 * 94; ++cell) { - if (JisX0208::TO_UCS2[cell] != 0) entries.push_back({JisX0208::TO_UCS2[cell], static_cast(cell), false}); - } - for (uint32_t cell = 0; cell < 94 * 94; ++cell) { - if (JisX0212::TO_UCS2[cell] != 0) entries.push_back({JisX0212::TO_UCS2[cell], static_cast(cell), true}); - } - std::stable_sort(entries.begin(), entries.end(), [](const JisCell& left, const JisCell& right) { return left.code < right.code; }); - entries.erase(std::unique(entries.begin(), entries.end(), [](const JisCell& left, const JisCell& right) { return left.code == right.code; }), entries.end()); - return entries; - }(); +std::vector buildJisReverse(bool strict) { + std::vector entries; + entries.reserve(2 * 94 * 94); + for (uint32_t cell = 0; cell < 94 * 94; ++cell) { + const uint16_t code = strict ? strictJisX0208(cell) : JisX0208::TO_UCS2[cell]; + if (code != 0) entries.push_back({code, static_cast(cell), false}); + } + for (uint32_t cell = 0; cell < 94 * 94; ++cell) { + if (JisX0212::TO_UCS2[cell] != 0) entries.push_back({JisX0212::TO_UCS2[cell], static_cast(cell), true}); + } + std::stable_sort(entries.begin(), entries.end(), [](const JisCell& left, const JisCell& right) { return left.code < right.code; }); + entries.erase(std::unique(entries.begin(), entries.end(), [](const JisCell& left, const JisCell& right) { return left.code == right.code; }), entries.end()); + return entries; +} + +const std::vector& jisReverse(bool strict) { + if (strict) { + static const auto strictReverse = buildJisReverse(true); + return strictReverse; + } + static const auto reverse = buildJisReverse(false); return reverse; } @@ -496,6 +566,8 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng return unicodeToSbc(static_cast(profile), code, mbcs[0]) ? 0 : CES_ERROR_UNASSIGNED_CODE; } if (isSjis(profile)) { + const bool strict = isSjisX0208(profile); + if (strict) requireSettledSjisX0208(code); if (code < 0x80) { length = 1; mbcs[0] = static_cast(code); @@ -507,7 +579,8 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng return 0; } if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; - const auto& reverse = jisReverse(); + if (strict && code == JIS_X0208_UNSETTLED_CODE) throw std::runtime_error(JIS_X0208_UNSETTLED); + const auto& reverse = jisReverse(strict); const auto found = std::lower_bound(reverse.begin(), reverse.end(), static_cast(code), [](const JisCell& entry, uint16_t value) { return entry.code < value; }); if (found == reverse.end() || found->code != code || found->supplementary) return CES_ERROR_UNASSIGNED_CODE; jisToSjis(found->cell / 94 + 1, found->cell % 94 + 1, mbcs[0], mbcs[1]); @@ -520,15 +593,18 @@ int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& leng return 0; } if (code >= 0xFF61 && code <= 0xFF9F) { + if (!hasSs2(profile)) return CES_ERROR_UNASSIGNED_CODE; length = 2; mbcs[0] = 0x8E; mbcs[1] = static_cast(0xA1 + (code - 0xFF61)); return 0; } if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; - const auto& reverse = jisReverse(); + const bool strict = isEucJpX0208(profile); + if (strict && code == JIS_X0208_UNSETTLED_CODE) throw std::runtime_error(JIS_X0208_UNSETTLED); + const auto& reverse = jisReverse(strict); const auto found = std::lower_bound(reverse.begin(), reverse.end(), static_cast(code), [](const JisCell& entry, uint16_t value) { return entry.code < value; }); - if (found == reverse.end() || found->code != code) return CES_ERROR_UNASSIGNED_CODE; + if (found == reverse.end() || found->code != code || (found->supplementary && !hasSs3(profile))) return CES_ERROR_UNASSIGNED_CODE; if (found->supplementary) { length = 3; mbcs[0] = 0x8F; @@ -942,6 +1018,12 @@ void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void* sheet) { return sheet; } +void* APS5_VABI sceCesUcsProfileInitSJis1997X0208(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, SJIS_X0208_TAG, sizeof(SJIS_X0208_TAG)); + return sheet; +} + int APS5_VABI sceCesMbcsUcsContextInit(void* context, const void* profile) { if (!context) return CES_ERROR_INVALID_PARAMETER; std::memcpy(context, &profile, sizeof(profile)); @@ -961,6 +1043,24 @@ void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void* sheet) { return sheet; } +void* APS5_VABI sceCesUcsProfileInitEucJpX0208(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, EUC_JP_X0208_TAG, sizeof(EUC_JP_X0208_TAG)); + return sheet; +} + +void* APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, EUC_JP_X0208_SS2_TAG, sizeof(EUC_JP_X0208_SS2_TAG)); + return sheet; +} + +void* APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2Ss3(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, EUC_JP_X0208_SS3_TAG, sizeof(EUC_JP_X0208_SS3_TAG)); + return sheet; +} + void* APS5_VABI sceCesUcsProfileInitEucKr(void* sheet) { if (!sheet) return nullptr; std::memcpy(sheet, EUC_KR_TAG, sizeof(EUC_KR_TAG)); diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index c125d6b76b..3680b257df 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -257,21 +257,6 @@ int APS5_VABI sceCesUcsProfileInitEucJis2004(void) { return 0; } -int APS5_VABI sceCesUcsProfileInitEucJpX0208(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2Ss3(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitGb18030_2000(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -297,11 +282,6 @@ int APS5_VABI sceCesUcsProfileInitIso2022JpRfc1468(void) { return 0; } -int APS5_VABI sceCesUcsProfileInitSJis1997X0208(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitSJis2004X0213(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestCesJapanese.cpp b/core/libs/tests/GuestCesJapanese.cpp index 2103b87187..754471576e 100644 --- a/core/libs/tests/GuestCesJapanese.cpp +++ b/core/libs/tests/GuestCesJapanese.cpp @@ -2,10 +2,21 @@ #include #include #include +#include +#include extern "C" { void* APS5_VABI sceCesUcsProfileInitSJis1997Cp932(void*); void* APS5_VABI sceCesUcsProfileInitEucJpCp51932(void*); +void* APS5_VABI sceCesUcsProfileInitEucJpX0208(void*); +void* APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2(void*); +void* APS5_VABI sceCesUcsProfileInitEucJpX0208Ss2Ss3(void*); +void* APS5_VABI sceCesUcsProfileInitSJis1997X0208(void*); +int APS5_VABI sceCesMbcsUcsContextInit(void*, const void*); +int APS5_VABI sceCesMbcsStrToUcs2Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUcs2StrToMbcsStr(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesEucJpToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesEucJpToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); const std::uint8_t* APS5_VABI sceCesRefersUcsProfileCp1252(void); int APS5_VABI sceCesSJisToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); int APS5_VABI sceCesSJisToUtf8(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); @@ -36,14 +47,242 @@ static void Require(bool value) { if (!value) std::abort(); } static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } +constexpr int invalidParameter = static_cast(0x805C0001); +constexpr int srcBufferEnd = static_cast(0x805C0011); +constexpr int invalidEncode = static_cast(0x805C0014); +constexpr int unassignedCode = static_cast(0x805C0020); +constexpr int dstBufferEnd = static_cast(0x805C0031); +constexpr std::uint32_t jisX0208Cells = 6878; +constexpr std::uint32_t jisX0208Digest = 0xA614D6F6; +constexpr std::uint32_t jisX0212Cells = 6067; +constexpr std::uint32_t jisX0212Digest = 0x7C193A73; +const std::string unsettledCell = "libSceCesCs: the console's mapping of JIS X 0208 cell 0x2140 (U+005C or U+FF3C) is unknown"; +const std::string unsettledByte = "libSceCesCs: the console's mapping of Shift-JIS bytes 0x5C and 0x7E (ASCII or JIS X 0201 Roman) is unknown"; + +template +static bool Reports(const std::string& message, TCall call) { + try { + call(); + } catch (const std::runtime_error& error) { + return error.what() == message; + } + return false; +} + +static std::uint32_t Digest(std::uint32_t hash, std::uint16_t value) { + hash = (hash ^ (value & 0xFF)) * 0x01000193u; + return (hash ^ (value >> 8)) * 0x01000193u; +} + +static int EucToUcs2(const void* profile, const char* bytes, std::uint32_t size, std::uint16_t& ucs2) { + std::uint32_t used = 0; + return sceCesEucJpToUcs2(profile, Bytes(bytes), size, &used, &ucs2); +} + +static int SJisToUcs2(const void* profile, const char* bytes, std::uint32_t size, std::uint16_t& ucs2) { + std::uint32_t used = 0; + return sceCesSJisToUcs2(profile, Bytes(bytes), size, &used, &ucs2); +} + +static bool EucEncodes(const void* profile, std::uint16_t ucs2, const char* expected) { + std::uint8_t mbcs[4] = {}; + std::uint32_t size = 0; + return sceCesUcs2ToEucJp(ucs2, profile, mbcs, sizeof(mbcs), &size) == 0 && size == std::strlen(expected) && std::memcmp(mbcs, expected, size) == 0; +} + +static bool SJisEncodes(const void* profile, std::uint16_t ucs2, const char* expected) { + std::uint8_t mbcs[4] = {}; + std::uint32_t size = 0; + return sceCesUcs2ToSJis(ucs2, profile, mbcs, sizeof(mbcs), &size) == 0 && size == std::strlen(expected) && std::memcmp(mbcs, expected, size) == 0; +} + +static bool EncodeLeavesOutputs(int expected, const void* profile, std::uint16_t ucs2, bool sjis) { + std::uint8_t mbcs[4] = {0xEE, 0xEE, 0xEE, 0xEE}; + std::uint32_t size = 77; + const int result = sjis ? sceCesUcs2ToSJis(ucs2, profile, mbcs, sizeof(mbcs), &size) : sceCesUcs2ToEucJp(ucs2, profile, mbcs, sizeof(mbcs), &size); + return result == expected && size == 77 && mbcs[0] == 0xEE && mbcs[1] == 0xEE && mbcs[2] == 0xEE; +} + +static bool DecodeLeavesOutputs(int expected, const void* profile, const char* bytes, std::uint32_t size, bool sjis) { + std::uint32_t used = 77; + std::uint16_t ucs2 = 0xEEEE; + const int result = sjis ? sceCesSJisToUcs2(profile, Bytes(bytes), size, &used, &ucs2) : sceCesEucJpToUcs2(profile, Bytes(bytes), size, &used, &ucs2); + return result == expected && used == 77 && ucs2 == 0xEEEE; +} + +static void SweepEucJisX0208(const void* profile) { + std::uint32_t assigned = 0; + std::uint32_t hash = 0x811C9DC5u; + for (std::uint32_t lead = 0xA1; lead <= 0xFE; ++lead) { + for (std::uint32_t trail = 0xA1; trail <= 0xFE; ++trail) { + const std::uint8_t cell[2] = {static_cast(lead), static_cast(trail)}; + std::uint32_t used = 77; + std::uint16_t ucs2 = 0xEEEE; + std::uint16_t value = 0; + if (lead == 0xA1 && trail == 0xC0) { + Require(Reports(unsettledCell, [&] { sceCesEucJpToUcs2(profile, cell, 2, &used, &ucs2); })); + } else if (const int result = sceCesEucJpToUcs2(profile, cell, 2, &used, &ucs2); result == 0) { + Require(used == 2); + value = ucs2; + ++assigned; + std::uint8_t again[4] = {}; + std::uint32_t size = 0; + Require(sceCesUcs2ToEucJp(ucs2, profile, again, sizeof(again), &size) == 0 && size == 2 && again[0] == lead && again[1] == trail); + } else { + Require(result == unassignedCode && used == 77 && ucs2 == 0xEEEE); + } + hash = Digest(hash, value); + } + } + Require(assigned == jisX0208Cells && hash == jisX0208Digest); +} + +static void SweepSJisJisX0208(const void* profile) { + std::uint32_t assigned = 0; + std::uint32_t hash = 0x811C9DC5u; + for (int row = 1; row <= 94; ++row) { + for (int column = 1; column <= 94; ++column) { + std::uint16_t code = 0; + Require(sceCesJisX0208ToSJisCode(static_cast(row), static_cast(column), &code) == 0); + const std::uint8_t cell[2] = {static_cast(code >> 8), static_cast(code)}; + std::uint32_t used = 77; + std::uint16_t ucs2 = 0xEEEE; + std::uint16_t value = 0; + if (row == 1 && column == 32) { + Require(Reports(unsettledCell, [&] { sceCesSJisToUcs2(profile, cell, 2, &used, &ucs2); })); + } else if (const int result = sceCesSJisToUcs2(profile, cell, 2, &used, &ucs2); result == 0) { + Require(used == 2); + value = ucs2; + ++assigned; + std::uint8_t again[4] = {}; + std::uint32_t size = 0; + Require(sceCesUcs2ToSJis(ucs2, profile, again, sizeof(again), &size) == 0 && size == 2 && again[0] == cell[0] && again[1] == cell[1]); + } else { + Require(result == unassignedCode && used == 77 && ucs2 == 0xEEEE); + } + hash = Digest(hash, value); + } + } + Require(assigned == jisX0208Cells && hash == jisX0208Digest); +} + +static void TestJisX0208Profiles(const void* cp932, const void* cp51932) { + alignas(8) std::uint8_t sheets[4][256] = {}; + Require(sceCesUcsProfileInitEucJpX0208(nullptr) == nullptr && sceCesUcsProfileInitEucJpX0208Ss2(nullptr) == nullptr); + Require(sceCesUcsProfileInitEucJpX0208Ss2Ss3(nullptr) == nullptr && sceCesUcsProfileInitSJis1997X0208(nullptr) == nullptr); + const void* euc = sceCesUcsProfileInitEucJpX0208(sheets[0]); + const void* ss2 = sceCesUcsProfileInitEucJpX0208Ss2(sheets[1]); + const void* ss3 = sceCesUcsProfileInitEucJpX0208Ss2Ss3(sheets[2]); + const void* sjis = sceCesUcsProfileInitSJis1997X0208(sheets[3]); + Require(euc == sheets[0] && ss2 == sheets[1] && ss3 == sheets[2] && sjis == sheets[3]); + const void* strictEuc[3] = {euc, ss2, ss3}; + + std::uint16_t ucs2 = 0; + Require(EucToUcs2(sjis, "\xA4\xA2", 2, ucs2) == invalidParameter); + Require(SJisToUcs2(euc, "\x82\xA0", 2, ucs2) == invalidParameter); + Require(EncodeLeavesOutputs(invalidParameter, sjis, 0x3042, false) && EncodeLeavesOutputs(invalidParameter, ss3, 0x3042, true)); + + for (const void* profile : strictEuc) { + SweepEucJisX0208(profile); + Require(EucToUcs2(profile, "\\~", 2, ucs2) == 0 && ucs2 == 0x5C && EucEncodes(profile, 0x5C, "\\") && EucEncodes(profile, 0x7E, "~")); + Require(EucToUcs2(profile, "\xA1\xC1", 2, ucs2) == 0 && ucs2 == 0x301C && EucEncodes(profile, 0x301C, "\xA1\xC1")); + Require(EucToUcs2(profile, "\xA1\xC2", 2, ucs2) == 0 && ucs2 == 0x2016); + Require(EucToUcs2(profile, "\xA1\xDD", 2, ucs2) == 0 && ucs2 == 0x2212); + Require(EucToUcs2(profile, "\xA1\xF1", 2, ucs2) == 0 && ucs2 == 0x00A2); + Require(EucToUcs2(profile, "\xA1\xF2", 2, ucs2) == 0 && ucs2 == 0x00A3); + Require(EucToUcs2(profile, "\xA2\xCC", 2, ucs2) == 0 && ucs2 == 0x00AC); + for (const std::uint16_t cp932Form : {0xFF5E, 0x2225, 0xFF0D, 0xFFE0, 0xFFE1, 0xFFE2, 0x2460, 0x2170}) Require(EncodeLeavesOutputs(unassignedCode, profile, cp932Form, false)); + Require(Reports(unsettledCell, [&] { EucEncodes(profile, 0xFF3C, ""); })); + Require(DecodeLeavesOutputs(unassignedCode, profile, "\xAD\xA1", 2, false) && DecodeLeavesOutputs(unassignedCode, profile, "\xF9\xA1", 2, false)); + Require(DecodeLeavesOutputs(unassignedCode, profile, "\xA9\xA1", 2, false) && DecodeLeavesOutputs(unassignedCode, profile, "\xFE\xFE", 2, false)); + Require(DecodeLeavesOutputs(invalidEncode, profile, "\xA4\x41", 2, false) && DecodeLeavesOutputs(invalidEncode, profile, "\xA4\xFF", 2, false)); + Require(DecodeLeavesOutputs(invalidEncode, profile, "\x80", 1, false) && DecodeLeavesOutputs(invalidEncode, profile, "\xFF", 1, false)); + Require(DecodeLeavesOutputs(srcBufferEnd, profile, "\xA4\xA2", 1, false)); + std::uint8_t mbcs[4] = {0xEE, 0xEE, 0xEE, 0xEE}; + std::uint32_t size = 77; + Require(sceCesUcs2ToEucJp(0x3042, profile, mbcs, 1, &size) == dstBufferEnd && size == 77 && mbcs[0] == 0xEE); + } + Require(EucToUcs2(cp51932, "\xA1\xC0", 2, ucs2) == 0 && ucs2 == 0xFF3C && EucEncodes(cp51932, 0xFF3C, "\xA1\xC0")); + Require(EucToUcs2(cp51932, "\xA1\xC1", 2, ucs2) == 0 && ucs2 == 0xFF5E && EucToUcs2(cp51932, "\xAD\xA1", 2, ucs2) == 0 && ucs2 == 0x2460); + Require(EucEncodes(cp51932, 0x7E8A, "\xF9\xA1") && EucEncodes(ss3, 0x7E8A, "\x8F\xD4\xE3")); + Require(EncodeLeavesOutputs(unassignedCode, euc, 0x7E8A, false) && EncodeLeavesOutputs(unassignedCode, ss2, 0x7E8A, false)); + + Require(DecodeLeavesOutputs(invalidEncode, euc, "\x8E\xB1", 2, false) && DecodeLeavesOutputs(invalidEncode, euc, "\x8E", 1, false)); + Require(DecodeLeavesOutputs(invalidEncode, euc, "\x8F\xB0\xA1", 3, false) && DecodeLeavesOutputs(invalidEncode, ss2, "\x8F\xB0\xA1", 3, false)); + Require(EncodeLeavesOutputs(unassignedCode, euc, 0xFF71, false)); + Require(EncodeLeavesOutputs(unassignedCode, euc, 0x4E02, false) && EncodeLeavesOutputs(unassignedCode, ss2, 0x4E02, false)); + for (const void* profile : {ss2, ss3}) { + for (std::uint32_t kana = 0xA1; kana <= 0xDF; ++kana) { + const char bytes[3] = {'\x8E', static_cast(kana), 0}; + Require(EucToUcs2(profile, bytes, 2, ucs2) == 0 && ucs2 == 0xFF61 + kana - 0xA1 && EucEncodes(profile, ucs2, bytes)); + } + Require(DecodeLeavesOutputs(invalidEncode, profile, "\x8E\xE0", 2, false) && DecodeLeavesOutputs(invalidEncode, profile, "\x8E\xA0", 2, false)); + Require(DecodeLeavesOutputs(srcBufferEnd, profile, "\x8E\xB1", 1, false)); + } + std::uint32_t supplementary = 0; + std::uint32_t hash = 0x811C9DC5u; + for (std::uint32_t lead = 0xA1; lead <= 0xFE; ++lead) { + for (std::uint32_t trail = 0xA1; trail <= 0xFE; ++trail) { + const char bytes[4] = {'\x8F', static_cast(lead), static_cast(trail), 0}; + std::uint16_t value = 0; + if (const int result = EucToUcs2(ss3, bytes, 3, ucs2); result == 0) { + value = ucs2; + ++supplementary; + Require(EucEncodes(ss3, ucs2, ucs2 == 0x7E ? "~" : bytes)); + } else { + Require(result == unassignedCode); + } + hash = Digest(hash, value); + } + } + Require(supplementary == jisX0212Cells && hash == jisX0212Digest); + Require(DecodeLeavesOutputs(srcBufferEnd, ss3, "\x8F\xB0", 2, false) && DecodeLeavesOutputs(invalidEncode, ss3, "\x8F\xB0\x41", 3, false)); + Require(EucEncodes(ss3, 0x4E02, "\x8F\xB0\xA1") && EucEncodes(ss2, 0xFF71, "\x8E\xB1")); + + std::uint8_t utf8[4] = {}; + std::uint32_t used = 0; + std::uint32_t produced = 0; + Require(sceCesEucJpToUtf8(euc, Bytes("\xA1\xC1"), 2, &used, utf8, sizeof(utf8), &produced) == 0 && used == 2 && produced == 3 && std::memcmp(utf8, "\xE3\x80\x9C", 3) == 0); + alignas(8) std::uint8_t context[256] = {}; + std::uint16_t wide[8] = {}; + Require(sceCesMbcsUcsContextInit(context, ss2) == 0); + Require(sceCesMbcsStrToUcs2Str(context, Bytes("A\xA4\xA2\x8E\xB1"), 5, &used, wide, 8, &produced) == 0 && used == 5 && produced == 3 && wide[1] == 0x3042 && wide[2] == 0xFF71); + Require(sceCesMbcsUcsContextInit(context, euc) == 0); + Require(sceCesMbcsStrToUcs2Str(context, Bytes("A\xA4\xA2\x8E\xB1"), 5, &used, wide, 8, &produced) == invalidEncode); + + SweepSJisJisX0208(sjis); + Require(SJisToUcs2(sjis, "\x81\x60", 2, ucs2) == 0 && ucs2 == 0x301C && SJisEncodes(sjis, 0x301C, "\x81\x60")); + Require(SJisToUcs2(sjis, "\x81\x91", 2, ucs2) == 0 && ucs2 == 0x00A2 && SJisEncodes(sjis, 0x00AC, "\x81\xCA")); + for (const std::uint16_t cp932Form : {0xFF5E, 0x2225, 0xFF0D, 0xFFE0, 0xFFE1, 0xFFE2, 0x2460, 0x2170, 0x7E8A, 0x4E02}) Require(EncodeLeavesOutputs(unassignedCode, sjis, cp932Form, true)); + for (const char* removed : {"\x87\x40", "\xED\x40", "\xEE\xFC", "\xFA\x40", "\xFC\x4B", "\xF0\x40", "\xEF\xFC"}) Require(DecodeLeavesOutputs(unassignedCode, sjis, removed, 2, true)); + Require(SJisToUcs2(cp932, "\x87\x40", 2, ucs2) == 0 && ucs2 == 0x2460 && SJisToUcs2(cp932, "\xFA\x40", 2, ucs2) == 0 && ucs2 == 0x2170); + Require(SJisToUcs2(cp932, "\x81\x5F", 2, ucs2) == 0 && ucs2 == 0xFF3C && SJisToUcs2(cp932, "\\", 1, ucs2) == 0 && ucs2 == 0x5C); + Require(DecodeLeavesOutputs(invalidEncode, sjis, "\x82\x7F", 2, true) && DecodeLeavesOutputs(invalidEncode, sjis, "\x82\xFD", 2, true)); + Require(DecodeLeavesOutputs(srcBufferEnd, sjis, "\x82\xA0", 1, true)); + for (std::uint32_t byte = 0; byte <= 0xFF; ++byte) { + const char bytes[2] = {static_cast(byte), 0}; + if (byte == 0x5C || byte == 0x7E) { + Require(Reports(unsettledByte, [&] { SJisToUcs2(sjis, bytes, 1, ucs2); })); + } else if (byte < 0x80) { + Require(SJisToUcs2(sjis, bytes, 1, ucs2) == 0 && ucs2 == byte && (byte == 0 || SJisEncodes(sjis, ucs2, bytes))); + } else if (byte >= 0xA1 && byte <= 0xDF) { + Require(SJisToUcs2(sjis, bytes, 1, ucs2) == 0 && ucs2 == 0xFF61 + byte - 0xA1 && SJisEncodes(sjis, ucs2, bytes)); + } else { + Require(DecodeLeavesOutputs(byte == 0x80 || byte == 0xA0 || byte >= 0xFD ? invalidEncode : srcBufferEnd, sjis, bytes, 1, true)); + } + } + for (const std::uint16_t unsettled : {0x5C, 0x7E, 0xA5, 0x203E}) Require(Reports(unsettledByte, [&] { SJisEncodes(sjis, unsettled, ""); })); + Require(Reports(unsettledCell, [&] { SJisEncodes(sjis, 0xFF3C, ""); })); + Require(sceCesMbcsUcsContextInit(context, sjis) == 0); + const std::uint16_t text[] = {0x3042, 0x301C, 0xFF71, 0}; + std::uint8_t mbcs[8] = {}; + Require(sceCesUcs2StrToMbcsStr(context, text, 4, &used, mbcs, sizeof(mbcs), &produced) == 0 && used == 3 && produced == 5 && std::memcmp(mbcs, "\x82\xA0\x81\x60\xB1", 5) == 0); + Require(Reports(unsettledByte, [&] { sceCesMbcsStrToUcs2Str(context, Bytes("A\\"), 2, &used, wide, 8, &produced); })); +} + int main() { - constexpr int invalidParameter = static_cast(0x805C0001); constexpr int invalidSrcBuffer = static_cast(0x805C0010); - constexpr int srcBufferEnd = static_cast(0x805C0011); - constexpr int invalidEncode = static_cast(0x805C0014); - constexpr int unassignedCode = static_cast(0x805C0020); constexpr int invalidDstBuffer = static_cast(0x805C0030); - constexpr int dstBufferEnd = static_cast(0x805C0031); alignas(8) std::uint8_t sjisSheet[64] = {}; alignas(8) std::uint8_t eucSheet[64] = {}; @@ -166,5 +405,6 @@ int main() { Require(sceCesSJisCodeToJisX0208(sjisCode, &ku, &ten) == 0 && ku == row && ten == cell); } } + TestJisX0208Profiles(sjis, euc); return 0; } diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 8e967e5b14..6c90ed380d 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -11,10 +11,11 @@ - [Unicode scalar conversions](../../../core/libs/prx/libSceCesCs/Unicode.cpp) (libSceCesCs) - console error precedence, optional length pointers and length units are unverified. Signatures follow the [Orbital type database](https://github.com/AlexAltea/orbital/blob/b321fcdbd700cc02da1cb1ad25412781d5ec73c6/tools/ida/db_types.json); names are in the aerolib table pinned by `tools/nid_names.py`. Encoding follows [Unicode section 3.9](https://www.unicode.org/versions/Unicode17.0.0/core-spec/chapter-3/). Each call converts one scalar, including zero and U+FEFF, without a terminator or byte-order detection; lengths count bytes for UTF-8 and 16-bit units for UTF-16. Native forms use host byte order (supported hosts are little-endian). Errors follow the existing CES converters, not console measurements, and failure leaves outputs unchanged. No console or title comparison has been run - [sceCesEucJpToUtf8](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the scalar conversion's errors and optional length pointers are unverified against the console. Its signature follows the Orbital type database linked above; it uses the existing CP51932 profile and JIS tables, with their mapping and profile-layout assumptions listed above, converts one character (including zero), and writes no terminator. Failure leaves the output and length pointers unchanged - [Shift-JIS and EUC-JP scalar conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSJisTo*`, `sceCesEucJpTo{Utf16,Utf32}*` and `sceCes{Ucs2,Utf8,Utf16,Utf32}*To{SJis,EucJp}` are unverified against the console. Signatures follow the Orbital type database linked above; the CP932 and CP51932 table and profile assumptions above apply. A profile of another encoding answers `INVALID_PARAMETER`, a surrogate or a value above U+10FFFF `INVALID_ENCODE`, a code point outside the BMP `UNASSIGNED_CODE`. `sceCesSJisCodeToJisX0208` and `sceCesJisX0208ToSJisCode` do not check that the cell is assigned; their `INVALID_ENCODE` for rows or cells outside 1-94 and `INVALID_DST_BUFFER` for a null output are not console measurements +- [Strict JIS X 0208 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - cell `0x2140` and U+FF3C throw: JIS0208.TXT gives U+005C, glibc 2.39 and Python's `euc_jp` U+FF3C. In `sceCesUcsProfileInitSJis1997X0208`, bytes `0x5C` and `0x7E` and U+005C, U+007E, U+00A5 and U+203E throw: SHIFTJIS.TXT gives JIS X 0201 Roman, CP932 and Python's `shift_jis` ASCII; the console's is unknown. The table, [JIS0208.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0208.TXT), is CP932's without rows 13 and 89-92 and with six cells changed, not measured. Per [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Text/Ces.cs#L57-L67), the `X0208Ss2` profile adds SS2 katakana and `X0208Ss2Ss3` SS3 JIS X 0212. Signatures follow Orbital; the conversions above take these profiles with the same errors, a missing SS2 or SS3 answering `INVALID_ENCODE` and Shift-JIS leads `0xF0`-`0xFC` `UNASSIGNED_CODE` - [UTF-16 and UTF-32 forms of the single-byte and MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSbcTo{Ucs2,Utf16,Utf32}*`, `sceCes{Ucs2,Utf16,Utf32}*ToSbc`, `sceCesMbcsStr{To,Get}{Utf16,Utf32}*` and `sceCes{Utf16,Utf32}Str{To,Get}Mbcs*` are unverified against the console. Signatures follow the Orbital type database linked above. They follow `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the UTF-8 MBCS string forms with the profiles and tables listed above. A surrogate or a value above U+10FFFF answers `INVALID_ENCODE`, a valid code point no profile maps `UNASSIGNED_CODE`. The string forms stop at a zero code unit, write no terminator and count code units - [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas, GB 18030 four-byte forms and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) - [Generic MBCS character and Shift-JIS code helpers](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesMbcTo*` convert one character with any profile the MBCS context accepts, as the per-encoding forms above do. `sceCesSJisGetCode` and `sceCesSJisPutCode` read and write one raw Shift-JIS code (one byte for ASCII and `0xA1`-`0xDF`) without checking that it is assigned. `sceCesSJisCodeToJisX0213` and `sceCesJisX0213ToSJisCode` use the Shift_JIS-2004 arithmetic of JIS X 0213 annex 1 (plane 2 rows 1, 3-5, 8, 12-15 and 78-94 on leads `0xF0`-`0xFC`). `sceCesMbcsUcsContextInitCopy` copies the stored profile pointer. Signatures follow the Orbital type database linked above; errors, their precedence and the optional size pointers follow the other converters and are unverified against the console - [Single-byte profiles](../../../core/libs/prx/libSceCesCs/SingleByte.hpp) (libSceCesCs) - the console's tables for these 27 profiles are not verified. Each returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with bytes 0x00-0x7F unchanged and the high half of a public table: ISO 8859-1, -2, -4, -5, -9, -10, -13, -14, -15 and -16 from [unicode.org](https://unicode.org/Public/MAPPINGS/ISO8859/), code pages 437, 737, 775, 850, 852, 855, 860-863, 865 and 866 from [Microsoft's PC tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/), KOI8-R and KOI8-U from [unicode.org](https://unicode.org/Public/MAPPINGS/VENDORS/MISC/), and 1254, 1256 and 1258 from Microsoft's [best-fit tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/), whose unassigned bytes map to C1 controls as in Cp1250. Profiles whose public table leaves bytes undefined or maps them to private use still throw -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 76 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 72 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 profile (`sceCes*UcsProfileSetSbcs`) is not implemented. User-defined areas (Private Use Area on Windows) answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The scalar calls to and from UCS-2 and UTF-8/16/32 convert one character as the Shift-JIS and EUC-JP scalar conversions do From 98df163cc638f6cb5aacbd3feae3be1423e14fa1 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Tue, 20 Oct 2026 16:06:33 +0500 Subject: [PATCH 292/318] fix(shader): widen LDS atomic fences to device scope on AMD's proprietary driver The recompiler orders an LDS atomic, and the compare-exchange loop that writes a byte or a halfword of LDS (ds_write_b8/b16), with a workgroup-scope OpMemoryBarrier after the update. AMD's proprietary Windows driver 32.0.21042.62 (Radeon 0x13c0, RDNA 2) compiles that memory barrier to s_barrier, as the code objects in its pipeline cache show. Such updates usually sit in branches that only some waves of a workgroup take, so those waves execute extra s_barriers, which pair with the other waves' barriers, and the other waves read LDS before it is written. An OpMemoryBarrier is not an execution barrier and SPIR-V allows it in non-uniform control flow, so the SPIR-V is valid; a device-scope OpMemoryBarrier compiles to GL0 and GL1 cache invalidations without s_barrier there. PS5LM Plus (PPSA99581, llama.cpp's Vulkan backend) drifted into garbage on that driver with flash attention on. Its flash-attention shader stores two halfwords to LDS in a branch that two of its four waves take, inside the KV loop. The driver's code had 23 s_barrier for the guest's 21; a replay of the dispatch differed from NVIDIA in 159 words, up to 1.7e38, and now has 21 and differs only in the last bit of 9 words. On that driver the fence is now device scope, which orders everything the workgroup-scope fence did. Other drivers keep the workgroup scope. The flag is part of the shader cache key, and agc_shader_replay takes --device-scope-lds-memory-barriers to replay a request the way that driver compiles it. The LDS update helper already fences, so the second, identical fence after it in ds_min/max_f32 and the other read-modify-write LDS operations is dropped. agc_driver_lds_subword_store_branch stores a halfword to LDS in a branch that waves 0-1 of four take, delays those waves and then exchanges LDS slots across one s_barrier. Before, thread 64 read a stale slot on the AMD driver; it passes on AMD and NVIDIA now. agc_subgroup_barrier checks the driver quirk and that ds_write_b16, ds_add_u32 and ds_max_f32 each emit one fence of the expected scope. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BM2kfDbr6MydxsfTckZJDR --- .../Execution/include/SubgroupBarrier.hpp | 4 + .../Execution/src/VulkanDevice.cpp | 3 + .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 1 + .../libSceAgcDriver/tests/SubgroupBarrier.cpp | 62 +++++++++++-- .../tests/execution/LdsSubwordStoreBranch.cpp | 92 +++++++++++++++++++ .../libSceAgcDriver/tools/ShaderReplay.cpp | 8 +- core/shader/recompiler/CacheKey.hpp | 1 + core/shader/recompiler/Recompiler.cpp | 1 + core/shader/recompiler/Recompiler.hpp | 1 + .../include/SpirvBackend/SpirvEmitter.hpp | 1 + .../SpirvBackend/SpirvEmitterHelpers.hpp | 4 +- .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvMemory/SpirvBufferAccess.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 1 + .../src/SpirvMemory/SpirvBufferAccess.cpp | 4 + .../SpirvMemory/SpirvMemoryInstructions.cpp | 7 +- docs/dev/debt/shader.md | 1 + 17 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp index 30f42ba2ab..8675650a20 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp @@ -9,6 +9,10 @@ inline bool SubgroupControlBarrierUsable(VkDriverId driver) { return driver != VK_DRIVER_ID_AMD_PROPRIETARY; } +inline bool WorkgroupScopeFenceUsable(VkDriverId driver) { + return driver != VK_DRIVER_ID_AMD_PROPRIETARY; +} + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index b87e5ba1a8..2b6a4871c4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -388,6 +388,7 @@ struct VulkanDevice::State { bool earlyFragmentSampleMaskTestBeforeSampleCounting = false; bool robustBufferAccess2 = false; bool subgroupControlBarriers = true; + bool workgroupScopeFences = true; // VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in // graphics stages, and compute workgroups wider than a wave). bool descriptorIndexing = false; @@ -1274,6 +1275,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE; state->fusedExactArithmetic = driver.driverID == VK_DRIVER_ID_MOLTENVK; state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID); + state->workgroupScopeFences = WorkgroupScopeFenceUsable(driver.driverID); } if (driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY && hasExtension(VK_NV_SHADER_SM_BUILTINS_EXTENSION_NAME)) { VkPhysicalDeviceShaderSMBuiltinsPropertiesNV smBuiltins{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SM_BUILTINS_PROPERTIES_NV}; @@ -3833,6 +3835,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.waveLdsMemoryBarriers = !state->subgroupControlBarriers; target.largePixelFunctionLdsBuffer = state->driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY; target.pixelFunctionStorageDwords = state->pixelFunctionStorageDwords; + target.deviceScopeLdsMemoryBarriers = !state->workgroupScopeFences; target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 44299ca46d..2b6dee3b5e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -750,6 +750,7 @@ void verifyKeySensitivity() { changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; }); changes("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = true; }); changes("wave LDS memory barriers", [](SampleRequest& sample) { sample.request.target.waveLdsMemoryBarriers = true; }); + changes("device-scope LDS memory barriers", [](SampleRequest& sample) { sample.request.target.deviceScopeLdsMemoryBarriers = true; }); changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; }); changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; }); changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; }); diff --git a/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp b/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp index 272e5ed914..0578ee283d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/SubgroupBarrier.cpp @@ -5,10 +5,12 @@ #include #include #include +#include #include #include #include #include +#include namespace { @@ -34,6 +36,10 @@ constexpr std::array BranchedLdsStep{ 0xbf8cc07fu, 0xbf8a0000u, 0xbf810000u, }; +constexpr std::array LdsHalfStore{0x34040082u, 0xd87c0400u, 0x00000102u, 0xbf8cc07fu, 0xbf810000u}; +constexpr std::array LdsAtomicAdd{0x34040082u, 0xd8000400u, 0x00000102u, 0xbf8cc07fu, 0xbf810000u}; +constexpr std::array LdsAtomicMax{0x34040082u, 0xd84c0400u, 0x00000102u, 0xbf8cc07fu, 0xbf810000u}; + struct Barriers { std::uint32_t subgroupControl = 0; std::uint32_t subgroupMemory = 0; @@ -43,14 +49,19 @@ void require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); } -Barriers recompile(std::uint32_t threads, bool waveLdsMemoryBarriers) { +struct MemoryBarriers { + std::uint32_t workgroup = 0; + std::uint32_t device = 0; +}; + +ShaderRecompiler::RecompileResult recompileProgram(std::span code, std::uint32_t threads, bool waveLdsMemoryBarriers, bool deviceScopeLdsMemoryBarriers) { using namespace ShaderRecompiler; const std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform}; const std::array extensions{"SPV_KHR_storage_buffer_storage_class"}; RecompileRequest request{}; - request.shader = {ShaderStage::Compute, 0x20000u, BranchedLdsStep, 0, {}}; + request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}}; request.context.waveSize = 32; - request.context.compute = ShaderComputeStageInfo{{threads, 1u, 1u}, 256u, {false, false, false}, false, 1u}; + request.context.compute = ShaderComputeStageInfo{{threads, 1u, 1u}, 512u, {false, false, false}, false, 1u}; request.target.vulkanVersion = 0x00403000u; request.target.spirvVersion = 0x00010600u; request.target.subgroupSize = 32; @@ -61,15 +72,42 @@ Barriers recompile(std::uint32_t threads, bool waveLdsMemoryBarriers) { request.target.maxWorkgroupInvocations = 1024; request.target.maxWorkgroupSharedMemoryBytes = 49152; request.target.waveLdsMemoryBarriers = waveLdsMemoryBarriers; + request.target.deviceScopeLdsMemoryBarriers = deviceScopeLdsMemoryBarriers; request.layout = {0, 0, 0, 128}; request.useCache = false; - const auto result = Recompile(request); - const auto& words = result.spirv.Words(); + return Recompile(request); +} + +std::unordered_map constantsOf(std::span words) { std::unordered_map constants; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) == spv::OpConstant && (words[at] >> 16u) == 4u) constants[words[at + 2]] = words[at + 3]; + } + return constants; +} + +MemoryBarriers memoryBarriers(std::span code, bool deviceScopeLdsMemoryBarriers) { + const auto result = recompileProgram(code, 128, false, deviceScopeLdsMemoryBarriers); + const auto& words = result.spirv.Words(); + const auto constants = constantsOf(words); + MemoryBarriers barriers; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) != spv::OpMemoryBarrier) continue; + const auto scope = constants.find(words[at + 1]); + require(scope != constants.end(), "a memory barrier scope is not a 32-bit constant"); + if (scope->second == spv::ScopeWorkgroup) ++barriers.workgroup; + if (scope->second == spv::ScopeDevice) ++barriers.device; + } + return barriers; +} + +Barriers recompile(std::uint32_t threads, bool waveLdsMemoryBarriers) { + const auto result = recompileProgram(BranchedLdsStep, threads, waveLdsMemoryBarriers, false); + const auto& words = result.spirv.Words(); + const auto constants = constantsOf(words); Barriers barriers; for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { const auto op = static_cast(words[at] & 0xffffu); - if (op == spv::OpConstant && (words[at] >> 16u) == 4u) constants[words[at + 2]] = words[at + 3]; if (op != spv::OpControlBarrier && op != spv::OpMemoryBarrier) continue; const auto scope = constants.find(words[at + 1]); require(scope != constants.end(), "a barrier scope is not a 32-bit constant"); @@ -83,6 +121,17 @@ Barriers recompile(std::uint32_t threads, bool waveLdsMemoryBarriers) { void verifyDriverQuirk() { for (const auto& item : Cases) { require(AgcDriver::SubgroupControlBarrierUsable(item.driver) == item.usable, "driver " + std::to_string(static_cast(item.driver)) + ": expected subgroup control barriers to be " + (item.usable ? "usable" : "unusable")); + require(AgcDriver::WorkgroupScopeFenceUsable(item.driver) == item.usable, "driver " + std::to_string(static_cast(item.driver)) + ": expected workgroup-scope fences to be " + (item.usable ? "usable" : "unusable")); + } +} + +void verifyLdsMemoryBarrierScope() { + const std::array>, 3> programs{{{"ds_write_b16", LdsHalfStore}, {"ds_add_u32", LdsAtomicAdd}, {"ds_max_f32", LdsAtomicMax}}}; + for (const auto& [name, code] : programs) { + const auto workgroup = memoryBarriers(code, false); + require(workgroup.workgroup == 1 && workgroup.device == 0, std::string(name) + " did not order its LDS update with one workgroup-scope OpMemoryBarrier"); + const auto device = memoryBarriers(code, true); + require(device.workgroup == 0 && device.device == 1, std::string(name) + " with device-scope LDS memory barriers did not order its LDS update with one device-scope OpMemoryBarrier"); } } @@ -101,6 +150,7 @@ int main() { try { verifyDriverQuirk(); verifyEmittedBarriers(); + verifyLdsMemoryBarrierScope(); std::puts("subgroup barrier tests passed"); return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp new file mode 100644 index 0000000000..ae9d55e2c0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp @@ -0,0 +1,92 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 128; +constexpr std::uint32_t BranchThreads = 64; +constexpr std::uint32_t Iterations = 0x1000; +constexpr std::uint32_t Rounds = 16; +constexpr std::uint32_t LdsDwords = 512; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array BranchCode{ + 0x34040082, 0xe0301000, 0x80000102, 0xbf8c3f70, 0xbe86037e, 0x7da800c0, 0xbf880002, 0xd87c0400, + 0x00000102, 0xbefe0306, 0x7e0e0500, 0xbf0ac007, 0x850580ff, 0x00001000, 0x7e0c0300, 0xbf068005, + 0xbf850005, 0xd5430006, 0x02050706, 0x80858105, 0xbf078005, 0xbf85fffb, 0xe0701200, 0x80000602, + 0xd8340000, 0x00000102, 0xbf8cc07f, 0xbf8a0000, 0x3a0604ff, 0x00000100, 0xd8d80000, 0x04000003, + 0xbf8cc07f, 0xe0701000, 0x80000402, 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +std::uint32_t BranchResult(std::uint32_t thread) { + std::uint32_t value = thread; + for (std::uint32_t iteration = 0; iteration < Iterations; ++iteration) value = (value & 0xffffffu) * 3u + 1u; + return value; +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + const auto target = device->ComputeTarget(32); + if (target.subgroupSize != 32u) { + std::printf("skipped, wave32 programs do not run on 32-lane subgroups (%u lanes)\n", target.subgroupSize); + return VulkanTestSkipped; + } + std::vector userData(4, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(output.begin(), output.end(), userData.begin()); + const std::span code(BranchCode); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, LdsDwords, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + const auto result = ShaderRecompiler::Recompile(request); + std::array input{}; + for (std::uint32_t round = 0; round < Rounds; ++round) { + for (std::uint32_t thread = 0; thread < Threads; ++thread) input[thread] = (round + 1u) * 0x10000u + thread; + Output.fill(0xdeadbeefu); + std::copy(input.begin(), input.end(), Output.begin()); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + const auto expected = input[thread ^ BranchThreads]; + Require(Output[thread] == expected, "LDS exchange after a 16-bit LDS store in a branch that waves 0-1 take, round " + std::to_string(round) + ": thread " + std::to_string(thread) + " (wave " + std::to_string(thread / 32u) + ") read " + std::to_string(Output[thread]) + ", expected " + std::to_string(expected)); + } + for (std::uint32_t thread = 0; thread < Threads; ++thread) { + const auto expected = thread < BranchThreads ? BranchResult(thread) : thread; + Require(Output[Threads + thread] == expected, "loop result of thread " + std::to_string(thread) + " in round " + std::to_string(round)); + } + } + std::puts("LDS subword store branch tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp index e4eeec9174..7bad5361bb 100644 --- a/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp +++ b/core/libs/prx/libSceAgcDriver/tools/ShaderReplay.cpp @@ -49,6 +49,7 @@ bool g_graph = false; bool g_maintenance8 = false; bool g_robust2 = false; bool g_waveLdsMemoryBarriers = false; +bool g_deviceScopeLdsMemoryBarriers = false; bool g_code = false; bool Replay(const char* path) { @@ -149,6 +150,7 @@ bool Replay(const char* path) { if (g_maintenance8) request.request.target.nonConstantImageOffsets = true; if (g_robust2) request.request.target.robustBufferAccess2 = true; if (g_waveLdsMemoryBarriers) request.request.target.waveLdsMemoryBarriers = true; + if (g_deviceScopeLdsMemoryBarriers) request.request.target.deviceScopeLdsMemoryBarriers = true; const auto result = ShaderRecompiler::Recompile(request.request); std::printf(" recompiled: %zu SPIR-V words\n", result.spirv.size()); if (g_spirv) { @@ -186,7 +188,7 @@ bool Replay(const char* path) { int main(int argc, char** argv) { if (argc < 2) { - std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] [--robust2] [--wave-lds-memory-barriers] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set; shader_
.prepare.req holds a request whose preparation failed before any resource capture, so replay it with --prepare\n"); + std::fprintf(stderr, "usage: agc_shader_replay [--dis] [--asm] [--cfg] [--mem] [--spv] [--prepare] [--materialize] [--code] [--maintenance8] [--robust2] [--wave-lds-memory-barriers] [--device-scope-lds-memory-barriers] ...\n the driver writes shader_
.req files when APS5_DUMP_SHADERS is set; shader_
.prepare.req holds a request whose preparation failed before any resource capture, so replay it with --prepare\n"); return 2; } int failures = 0; @@ -215,6 +217,10 @@ int main(int argc, char** argv) { g_waveLdsMemoryBarriers = true; continue; } + if (std::string(argv[i]) == "--device-scope-lds-memory-barriers") { + g_deviceScopeLdsMemoryBarriers = true; + continue; + } if (std::string(argv[i]) == "--mem") { g_memory = true; continue; diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 10aaf73ff4..530663b309 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -300,6 +300,7 @@ private: append(key, value.fusedExactArithmetic); append(key, value.largePixelFunctionLdsBuffer); append(key, value.pixelFunctionStorageDwords); + append(key, value.deviceScopeLdsMemoryBarriers); } }; diff --git a/core/shader/recompiler/Recompiler.cpp b/core/shader/recompiler/Recompiler.cpp index ef28af0ff8..6352afa4ee 100644 --- a/core/shader/recompiler/Recompiler.cpp +++ b/core/shader/recompiler/Recompiler.cpp @@ -642,6 +642,7 @@ CompiledVariant emitVariant(const RecompileRequest& request, IrProgram& program) if (budget.bytes > limits.maxOutputMemoryBytes) throw std::runtime_error("barycentric emulation on the mesh path needs " + std::to_string(budget.bytes) + " bytes of mesh output memory, the device allows " + std::to_string(limits.maxOutputMemoryBytes)); } targetOptions.waveLdsMemoryBarriers = request.target.waveLdsMemoryBarriers; + targetOptions.deviceScopeLdsMemoryBarriers = request.target.deviceScopeLdsMemoryBarriers; constexpr SpirvEmitter spirvEmitter; CompiledShaderArtifact result; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 7b9cc5ed0f..b4b8e47fb2 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -301,6 +301,7 @@ struct SpirvTarget { bool fusedExactArithmetic = false; bool largePixelFunctionLdsBuffer = false; std::uint32_t pixelFunctionStorageDwords = 4096u; + bool deviceScopeLdsMemoryBarriers = false; }; struct BindingLayout { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp index 9a418524a3..146fb2318a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitter.hpp @@ -41,6 +41,7 @@ struct SpirvTargetOptions { bool waveLdsMemoryBarriers = false; bool largePixelFunctionLdsBuffer = false; std::uint32_t pixelFunctionStorageDwords = 4096u; + bool deviceScopeLdsMemoryBarriers = false; }; struct SpirvEmitInfo { diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 34213a41b6..1c0d45a569 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -112,6 +112,7 @@ void EmitMemoryOffsets(SpirvEmitterState& state); std::uint32_t LdsDwordCount(const SpirvEmitterState& state); bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); +std::uint32_t LdsMemoryBarrierScope(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); @@ -276,7 +277,8 @@ std::uint32_t AtomicUpdateTyped(SpirvEmitterState& state, std::uint32_t pointer, EmitLabel(state, cont); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, merge); - if (kind != ResourceKind::Lds || !state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory)); + const auto fenceScope = kind == ResourceKind::Lds ? LdsMemoryBarrierScope(state) : scope; + if (kind != ResourceKind::Lds || !state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, fenceScope), ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | memory)); return observed; } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 38b321e595..1b6d7c5297 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -129,6 +129,7 @@ struct SpirvEmitterState { std::uint32_t pixelFlatPositions = 0; std::map, std::uint32_t> solvedBarycentrics; bool waveLdsMemoryBarriers = false; + bool deviceScopeLdsMemoryBarriers = false; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; std::uint32_t spirvVersion = 0x00010300u; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp index 53a851625b..18e703456f 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp @@ -13,6 +13,7 @@ void EmitMemoryOffsets(SpirvEmitterState& state); std::uint32_t LdsDwordCount(const SpirvEmitterState& state); bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); +std::uint32_t LdsMemoryBarrierScope(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 8fd7e67442..96ddd6f796 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -280,6 +280,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.waveLdsMemoryBarriers = target.waveLdsMemoryBarriers; state.float32DenormPreserve = target.float32DenormPreserve; state.fusedExactArithmetic = target.fusedExactArithmetic; + state.deviceScopeLdsMemoryBarriers = target.deviceScopeLdsMemoryBarriers; state.hostSubgroupSize = target.subgroupSize; if (target.largePixelFunctionLdsBuffer && program.Resources().stage == IrShaderStage::Pixel && state.requirements.functionLds && state.requirements.functionLdsDwords > target.pixelFunctionStorageDwords) { state.pixelFunctionLdsBuffer = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 1a0ebafe6d..e2f95214ee 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -113,6 +113,10 @@ void EmitPixelFunctionLdsBase(SpirvEmitterState& state) { state.ldsBufferBase = EmitAddU32(state, EmitBinaryU32(state, spv::OpIMul, within, ConstantU32(state, FunctionLdsDwordLimit)), ConstantU32(state, 3u)); } +std::uint32_t LdsMemoryBarrierScope(const SpirvEmitterState& state) { + return state.deviceScopeLdsMemoryBarriers ? spv::ScopeDevice : spv::ScopeWorkgroup; +} + void EmitLdsBufferBase(SpirvEmitterState& state) { EmitPixelFunctionLdsBase(state); if (!LdsInDeviceMemory(state)) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index cd82fa2998..a6e4c11678 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -916,7 +916,7 @@ std::uint32_t Atomic32(SpirvValueEmitContext& ctx, const IrValue& inst, const Me const auto old = EmitAtomicOperation(ctx, inst, pointer, scope); if (lds) { const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); - if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, scope), ConstantU32(state, semantics)); + if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, LdsMemoryBarrierScope(state)), ConstantU32(state, semantics)); } else { EmitDeviceAtomicMemoryBarrier(state); } @@ -1037,10 +1037,7 @@ std::uint32_t SharedAtomicUpdate(SpirvValueEmitContext& ctx, const IrValue& inst return EmitAtomicAccess(ctx, inst, mem, [&](std::uint32_t pointer) { const auto next = [&](std::uint32_t current) { return replacement(state, current); }; if (InFunctionStorage(state, mem.kind)) return UpdateFunctionElement(state, pointer, next); - const auto old = AtomicUpdate(state, pointer, mem.kind, next); - const std::uint32_t semantics = spv::MemorySemanticsAcquireReleaseMask | LdsMemorySemantics(state); - if (!state.insideLdsLock) state.module.AddFunction(spv::OpMemoryBarrier, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, semantics)); - return old; + return AtomicUpdate(state, pointer, mem.kind, next); }); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index da248bd79d..9546fa4ac0 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -183,3 +183,4 @@ - [Guest memory load guards](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (shader recompiler) - a guest load whose lane is inactive or whose address is out of bounds is still performed and its result replaced by 0 with `OpSelect`, instead of a selection per guard, when the load can be pointed at a location nothing writes: scratch reads its own element 0; storage buffers (direct buffers, GDS) read element 0xfffffffe, which `robustBufferAccess2` turns into a zero load with no memory access, so the extra read cannot race with a write the guarded program would not have raced with ([Vulkan memory model](https://docs.vulkan.org/spec/latest/appendices/memorymodel.html#data-race)). Without `robustBufferAccess2` ([VulkanDevice.cpp](../../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) enables it when the device has it) those loads keep their selections; LDS loads, stores, atomics, formatted and physical-address (BDA) accesses always do. LDS keeps the branch because NVIDIA's compiler predicates a branched LDS load at no cost, while the select form costs about 2.5 instructions per dword and splits a `ds_read_b128` into four loads (RTX 4060 measurements by @karanvora2599 in #4048); Intel's static counts favour the select only for wide reads, and run time was not measured on either. The aligned high word of a misaligned-capable buffer read re-reads the low word's own element. A selection guard costs four blocks per lane half and the host compiler's time grows with the block count: PPSA11142's merged PSP vertex program (3176 ISA words, wave64) emitted 16,983 blocks and 1,710 conditional branches, 5,907 and 868 with the selects. `agc_guarded_memory_loads` traces the emitted loads in both device modes. - [Merged hull programs](../../../core/shader/recompiler/Optimization/src/TessellationLowering.cpp) (registered binary type 3) run whole in the hull stage, one invocation per output control point. On the console hull lane i keeps local lane i's registers, which with fewer input than output control points stop matching the patch's control points from i = inputs on; here invocation i holds control point min(i, inputs - 1) of its own patch. A wave-mask branch (`s_cbranch_exec[n]z`, `s_cbranch_vcc[n]z`) tests the invocation's own lane, so it can skip a block, scalar instructions included, that the console runs for the wave; `v_readfirstlane_b32` returns the invocation's own value, not the first active lane's, and another lane's register throws. The register layout (`s3`, `v0`-`v5`, as for the separate halves) is not measured on hardware. s[0:1] hold the hull user-data address (SH 0x102/0x103) when a draw sets it and zero otherwise, so a program reading it unset faults at the null address. - [Wave LDS ordering on AMD's proprietary driver](../../../core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp) (shader recompiler) - there ([SubgroupBarrier.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp)) one guest wave's LDS accesses are ordered by a subgroup-scope `OpMemoryBarrier` with no control barrier, relying on a 32-lane subgroup being one hardware wave whose lanes run in lockstep and issue LDS instructions in order, which Vulkan does not guarantee; for LDS kept in device memory the same is assumed of global loads and stores, unmeasured. Driver 32.0.21042.62 (Radeon 0x13c0, RDNA 2) compiled the subgroup-scope `OpControlBarrier` to `s_barrier`, which waves in a branch paired with other waves' barriers (PS5LM Plus). Measured on that driver only ([LdsTreeReduction.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsTreeReduction.cpp)); a Radeon 860M on 32.0.13058.8001 reportedly did not show the fault. +- [LDS atomic fences on AMD's proprietary driver](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp) (shader recompiler) - there ([SubgroupBarrier.hpp](../../../core/libs/prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp)) the fence after an LDS atomic or a sub-dword or misaligned LDS store is a device-scope `OpMemoryBarrier`, which that driver compiles to GL0 and GL1 cache invalidations; the cost is unmeasured. Driver 32.0.21042.62 (Radeon 0x13c0, RDNA 2) compiled the workgroup-scope fence to `s_barrier`, which waves in a branch paired with other waves' barriers (PS5LM Plus with flash attention). Measured on that driver only ([LdsSubwordStoreBranch.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/LdsSubwordStoreBranch.cpp)); a Radeon 860M (RDNA 3.5, 32.0.13058.8001) reportedly gave correct flash-attention output, but the gate covers every version. From d44c823834aa7f93eef3abf6f3ae0c22d2db1679 Mon Sep 17 00:00:00 2001 From: ntamas94 Date: Fri, 23 Oct 2026 20:46:58 +0200 Subject: [PATCH 293/318] feat(libSceJson2): implement sce::Json::Array::operator= Copy the source array into this and return this, as the console does (measured on a PS4 Pro, firmware 10.71). Knowledge is Power: Decades imports it; the inventory stub threw NotImplemented. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceJson2/Export.cpp | 8 ++++++ .../libs/prx/libSceJson2/InventoryExports.cpp | 6 ---- core/libs/tests/GuestJson.cpp | 28 +++++++++++++++++++ docs/dev/debt/libSceJson2.md | 1 - 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index a84b073cd9..529fee64d0 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -737,6 +737,14 @@ int APS5_VABI _ZNK3sce4Json6String7compareERKS1_(const String* self, const Strin void APS5_VABI _ZN3sce4Json5ArrayC1Ev(Array* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json5ArrayC1ERKS1_(Array* self, const Array* other) { CopyArray(*self, *other); } void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array* self) { Destroy(*self); } +Array* APS5_VABI _ZN3sce4Json5ArrayaSERKS1_(Array* self, const Array* other) { + if (self == other) return self; + Array copy{}; + CopyArray(copy, *other); + Destroy(*self); + *self = copy; + return self; +} void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array* self, const Value* value) { Value copy{}; CopyInto(copy, *value); diff --git a/core/libs/prx/libSceJson2/InventoryExports.cpp b/core/libs/prx/libSceJson2/InventoryExports.cpp index 5c89743172..51220c9a00 100644 --- a/core/libs/prx/libSceJson2/InventoryExports.cpp +++ b/core/libs/prx/libSceJson2/InventoryExports.cpp @@ -167,12 +167,6 @@ int APS5_VABI sceJson2Unknown_027_nid_no_patch() { } APS5_EXPORT("_ZN3sce4Json5ArrayD2Ev", sceJson2Unknown_027_nid_no_patch); -int APS5_VABI sceJson2Unknown_028_nid_no_patch() { - NotImplemented_nid_no_patch(__func__); - return 0; -} -APS5_EXPORT("_ZN3sce4Json5ArrayaSERKS1_", sceJson2Unknown_028_nid_no_patch); - int APS5_VABI sceJson2Unknown_029_nid_no_patch() { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index b2951558b6..9a577f0dd3 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -55,6 +55,7 @@ void APS5_VABI _ZN3sce4Json5Array5clearEv(Array*); const Value* APS5_VABI _ZNK3sce4Json5Array5frontEv(const Array*); Value* APS5_VABI _ZNK3sce4Json5Array8iteratorptEv(const Iterator*); void APS5_VABI _ZN3sce4Json5ArrayD1Ev(Array*); +Array* APS5_VABI _ZN3sce4Json5ArrayaSERKS1_(Array*, const Array*); void APS5_VABI _ZN3sce4Json5Array9push_backERKNS0_5ValueE(Array*, const Value*); Iterator* APS5_VABI _ZNK3sce4Json5Array5beginEv(Iterator*, const Array*); Iterator* APS5_VABI _ZNK3sce4Json5Array3endEv(Iterator*, const Array*); @@ -294,6 +295,32 @@ static void NestingDepth() { _ZN3sce4Json5ValueD1Ev(&root); } +static void ArrayAssignment() { + Array source{}, target{}; + _ZN3sce4Json5ArrayC1Ev(&source); + _ZN3sce4Json5ArrayC1Ev(&target); + Value flag{}, real{}; + _ZN3sce4Json5ValueC1Eb(&flag, true); + _ZN3sce4Json5ValueC1Ed(&real, 1.5); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&source, &flag); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&source, &real); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&source, &flag); + _ZN3sce4Json5Array9push_backERKNS0_5ValueE(&target, &real); + Require(_ZN3sce4Json5ArrayaSERKS1_(&target, &source) == &target); + Require(_ZNK3sce4Json5Array4sizeEv(&target) == 3); + Require(_ZNK3sce4Json5Array4sizeEv(&source) == 3); + _ZN3sce4Json5Array5clearEv(&source); + Require(_ZNK3sce4Json5Array4sizeEv(&target) == 3); + Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array5frontEv(&target))); + Require(*_ZNK3sce4Json5Value10getBooleanEv(_ZNK3sce4Json5Array4backEv(&target))); + Require(_ZN3sce4Json5ArrayaSERKS1_(&target, &target) == &target); + Require(_ZNK3sce4Json5Array4sizeEv(&target) == 3); + _ZN3sce4Json5ValueD1Ev(&flag); + _ZN3sce4Json5ValueD1Ev(&real); + _ZN3sce4Json5ArrayD1Ev(&source); + _ZN3sce4Json5ArrayD1Ev(&target); +} + static void ObjectsAndArrays() { Object object{}; _ZN3sce4Json6ObjectC1Ev(&object); @@ -1466,6 +1493,7 @@ int main(int argc, char** argv) { ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); + ArrayAssignment(); ObjectFind(); ObjectPair(); ObjectInsert(); diff --git a/docs/dev/debt/libSceJson2.md b/docs/dev/debt/libSceJson2.md index b9fac63328..527684f6a3 100644 --- a/docs/dev/debt/libSceJson2.md +++ b/docs/dev/debt/libSceJson2.md @@ -42,7 +42,6 @@ - [_ZN3sce4Json5ArrayC2ERKS1_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `6NlbpyIfUuY` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZN3sce4Json5ArrayC2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `t6AHhkWu4mY` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZN3sce4Json5ArrayD2Ev](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `MIyMPw7YTGM` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. -- [_ZN3sce4Json5ArrayaSERKS1_](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `w5UbvPLGye0` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZN3sce4Json5Value11referStringEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `m5J28iw-QPs` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZN3sce4Json5Value12referBooleanEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `QxjfcfBhEdc` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. - [_ZN3sce4Json5Value12referIntegerEv](../../../core/libs/prx/libSceJson2/InventoryExports.cpp) (libSceJson2) - NID `R0ac5QOdlpo` from the PS5 3.20 export inventory; prototype and behavior are unverified. Declaration is for import resolution only. From 19dd43f0d847c22e441d7a41dec265e7e0fb9126 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sat, 24 Oct 2026 08:03:40 +1300 Subject: [PATCH 294/318] test(libScePad): drop the unimplemented-throw asserts added by #9995 The tests for scePadSetLotusEnableIrKey, scePadSetLotusDisableIrKey, scePadVirtualDeviceDisableButtonRemapping and scePadVirtualDeviceGetRemoteSetting only checked that an unimplemented call throws. A test of that kind fails as soon as someone implements the function, so it pins the absence of a feature rather than behaviour. Tests should cover real behaviour and argument validation. This removes those asserts, the Throws helper and the declarations they were the only users of. The rest of the pad test is unchanged. --- core/libs/tests/GuestPad.cpp | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index e5573141da..35abd4888c 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -42,24 +41,10 @@ int APS5_VABI scePadGetExtControllerInformation(int, void*); int APS5_VABI scePadResetLightBar(int); int APS5_VABI scePadSetLightBar(int, const PadLightBarParam*); int APS5_VABI scePadSetMotionSensorState(int, bool); -int APS5_VABI scePadSetLotusEnableIrKey(std::int32_t); -int APS5_VABI scePadSetLotusDisableIrKey(std::int32_t); -int APS5_VABI scePadVirtualDeviceDisableButtonRemapping(bool); -int APS5_VABI scePadVirtualDeviceGetRemoteSetting(std::int32_t, void*); } static void Require(bool value) { if (!value) std::abort(); } -template -static bool Throws(TCall call, const char* function) { - try { - static_cast(call()); - } catch (const std::runtime_error& error) { - return std::strstr(error.what(), function) != nullptr; - } - return false; -} - static void CheckInputCommands() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-pad-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); struct Cleanup { @@ -603,8 +588,6 @@ int main() { Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); Require(scePadGetHandle(user, 0, 1) == noHandle); - Require(Throws([&] { return scePadSetLotusEnableIrKey(handle); }, "scePadSetLotusEnableIrKey")); - Require(Throws([&] { return scePadSetLotusDisableIrKey(handle); }, "scePadSetLotusDisableIrKey")); Require(scePadClose_nid_postfix(handle) == 0); Require(scePadGetHandle(user, 0, 0) == noHandle); Require(scePadUnknown_n3kSX62fgNo() == PAD_OK); @@ -614,15 +597,4 @@ int main() { Require(scePadSetAngularVelocityDeadbandState(handle + 1, true) == PAD_ERROR_INVALID_HANDLE); Require(scePadSetAngularVelocityBiasCorrectionState(handle, false) == PAD_OK); Require(scePadSetAngularVelocityBiasCorrectionState(handle + 1, false) == PAD_ERROR_INVALID_HANDLE); - Require(Throws([&] { return scePadSetLotusEnableIrKey(handle + 1); }, "scePadSetLotusEnableIrKey")); - Require(Throws([&] { return scePadSetLotusDisableIrKey(handle + 1); }, "scePadSetLotusDisableIrKey")); - Require(Throws([] { return scePadVirtualDeviceDisableButtonRemapping(true); }, "scePadVirtualDeviceDisableButtonRemapping")); - Require(Throws([] { return scePadVirtualDeviceDisableButtonRemapping(false); }, "scePadVirtualDeviceDisableButtonRemapping")); - std::array scratch{}; - scratch.fill(0xAB); - Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(1, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); - Require(Throws([] { return scePadVirtualDeviceGetRemoteSetting(1, nullptr); }, "scePadVirtualDeviceGetRemoteSetting")); - Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(0, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); - Require(Throws([&] { return scePadVirtualDeviceGetRemoteSetting(-1, scratch.data()); }, "scePadVirtualDeviceGetRemoteSetting")); - for (std::uint8_t byte : scratch) Require(byte == 0xAB); } From 80677584129eb0bd91383b5012be5e2b7247bafb Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 20 Oct 2026 16:51:20 +0100 Subject: [PATCH 295/318] fix(agc): recognise the copy kernel as Demon's Souls 01.007.000 compiles it The buffer copy HLE recognises its copy kernel (dst[i] = src[i % period] for i < count, count and period read from a parameter buffer) by a hash of its first 38 instruction words. Demon's Souls PPSA01341 01.007.000 ships the same kernel compiled to a different instruction sequence, with the same descriptors and parameter layout, so every one of its copies ran as a GPU dispatch and later CPU reads of the copied bytes waited for it. That hash is now accepted too. The new agc_driver_period_copy test runs that kernel through the driver: with the HLE the linear cases must take a copy path, a period below the count and a launch larger than the count needs must not, and every case must produce dst[i] = src[i % period]. agc_driver_period_copy_no_hle runs the same cases as shader dispatches (APS5_NO_COPY_HLE=1), so both paths are checked against the same results. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../src/Driver/Memory/BufferCopy.cpp | 4 +- .../tests/execution/PeriodCopy.cpp | 116 ++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PeriodCopy.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 75f7e48d00..c9ada84c07 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2454,6 +2454,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_draw_unsupported_buffer_type_skip_refused PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_period_copy_no_hle COMMAND agc_driver_period_copy_tests) + set_tests_properties(agc_driver_period_copy_no_hle PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_COPY_HLE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_period_copy_no_hle PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_sparse_indirect_indices_unreadable COMMAND agc_driver_sparse_indirect_indices_tests --unreadable) set_tests_properties(agc_driver_sparse_indirect_indices agc_driver_sparse_indirect_indices_unreadable PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp index 10d28032c9..9e8ca4fe94 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferCopy.cpp @@ -18,14 +18,14 @@ bool Driver::matchesCopyKernel(std::span code, const std::v static const bool enabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; if (!enabled || userData.size() != 12 || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1 || !compute.groupIdEnable[0]) return false; static constexpr std::size_t copyKernelWords = 38; - static constexpr std::uint64_t copyKernelHash = 0x6ec00fe8aa95f99aull; + static constexpr std::array copyKernelHashes{0x6ec00fe8aa95f99aull, 0xc771c42e4d069d62ull}; if (code.size() < copyKernelWords || code[0] != 0xbfa00002u || code[copyKernelWords - 1] != 0xbf810000u) return false; std::uint64_t hash = 0xcbf29ce484222325ull; for (const auto word : code.first(copyKernelWords)) { hash ^= word; hash *= 0x100000001b3ull; } - if (hash != copyKernelHash) return false; + if (std::find(copyKernelHashes.begin(), copyKernelHashes.end(), hash) == copyKernelHashes.end()) return false; const auto stride = [&](std::size_t word) { return (userData[word] >> 16u) & 0x3fffu; }; const auto swizzled = [&](std::size_t word) { return ((userData[word] >> 31u) & 1u) != 0; }; return userData[3] == 0x00014004u && userData[7] == 0x00014004u && userData[11] == 0x0004dfacu && stride(1) == 4 && stride(5) == 4 && stride(9) == 16 && !swizzled(1) && !swizzled(5) && !swizzled(9) && userData[10] >= 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PeriodCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PeriodCopy.cpp new file mode 100644 index 0000000000..0c4fecdb35 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PeriodCopy.cpp @@ -0,0 +1,116 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +alignas(256) constexpr std::array Code{ + 0xbfa00002u, 0xd7460000u, 0x04010c0cu, 0xf4201a84u, 0xfa000000u, 0xbf8cc07fu, 0x7da8006au, 0xbf88001du, + 0xf4200204u, 0xfa000004u, 0xbf8cc07fu, 0x7e020c08u, 0x81ea0880u, 0x7e025701u, 0x100202ffu, 0x4f7ffffeu, + 0x7e020f01u, 0x7ed60501u, 0x936a6b6au, 0x9aea6b6au, 0x4a02026au, 0xd56a0001u, 0x00020101u, 0xd5690001u, + 0x00020208u, 0x4c020300u, 0x7d860208u, 0x4e040208u, 0x02020501u, 0x7d860208u, 0x4e040208u, 0x02020501u, + 0xe0002000u, 0x80000101u, 0xbf8c3f70u, 0xe0102000u, 0x80010100u, 0xbf810000u}; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t ElementFormat = 0x00014004u; +constexpr std::uint32_t RecordFormat = 0x0004dfacu; +constexpr std::uint32_t Elements = 8192; +alignas(256) std::array Source{}; +alignas(256) std::array Destination{}; +alignas(256) std::array Record{}; + +void AppendDescriptor(std::vector& data, const void* base, std::uint32_t stride, std::uint32_t records, std::uint32_t format) { + const auto address = reinterpret_cast(base); + data.insert(data.end(), {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, format}); +} + +std::vector Commands(std::uint32_t groups) { + std::vector data; + AppendDescriptor(data, Source.data(), 4, Elements, ElementFormat); + AppendDescriptor(data, Destination.data(), 4, Elements, ElementFormat); + AppendDescriptor(data, Record.data(), 16, 1, RecordFormat); + std::vector words; + const auto registers = [&](std::uint32_t first, std::span values) { + words.push_back(0xc0007600u | (static_cast(values.size()) << 16u)); + words.push_back(first); + words.insert(words.end(), values.begin(), values.end()); + }; + const std::array shape{64, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Code.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{(static_cast(data.size()) << 1u) | (1u << 7u)}; + registers(0x213, resource); + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, groups, 1, 1, 0x41u}); + return words; +} + +std::uint64_t Taken() { + using namespace AgcDriver::DriverDetail; + return copyPathCounts[CopyCpu].load() + copyPathCounts[CopyGpu].load() + copyPathCounts[CopyAliased].load(); +} + +void ExecuteTests() { + struct Case { + const char* name; + std::uint32_t count, period, groups; + bool hle; + }; + constexpr std::array cases{{ + {"one full group", 64, 64, 1, true}, + {"a period above the count", 1000, 4096, 16, true}, + {"a partial last group", 100, 100, 2, true}, + {"a period below the count", 300, 7, 5, false}, + {"more groups than the count needs", 64, 64, 2, false}, + }}; + const bool hleEnabled = std::getenv("APS5_NO_COPY_HLE") == nullptr; + for (const auto& item : cases) { + const bool hle = item.hle && hleEnabled; + for (std::size_t i = 0; i < Source.size(); ++i) Source[i] = static_cast(i) * 2654435761u + 1u; + Destination.fill(Untouched); + Record = {item.count, item.period, 0, 0}; + const auto taken = Taken(); + const auto words = Commands(item.groups); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Destination.data()), sizeof(Destination)); + const std::string where = std::string(" (") + item.name + ")"; + Require(Taken() - taken == (hle ? 1u : 0u), (hle ? "the copy did not take the HLE path" : "the copy took the HLE path") + where); + for (std::size_t i = 0; i < Destination.size(); ++i) { + const auto expected = i < item.count ? Source[i % item.period] : Untouched; + Require(Destination[i] == expected, "copy mismatch at element " + std::to_string(i) + where + ": got " + std::to_string(Destination[i]) + ", expected " + std::to_string(expected)); + } + } +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + ExecuteTests(); + AgcDriverShutdown_nid_postfix(); + std::puts("period copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + AgcDriverShutdown_nid_postfix(); + return 1; + } +} From 548267bfa3d6b7b866d969b2eab1416d3bf3517b Mon Sep 17 00:00:00 2001 From: SuarezPM Date: Tue, 20 Oct 2026 21:37:25 -0300 Subject: [PATCH 296/318] feat(shader): support 32-bit image atomics through keyed image tables Table T#s keyed at runtime no longer fault on image_atomic_*32: the table operation stays supported, elements carry the atomic flag through dedup into the descriptor binding, and the table path binds AtomicView like the direct path. 64-bit atomics stay unsupported, packed and BY formats stay unsupported, and the debt entry records the relaxed Device-scoped emission and the remaining gaps. --- .../Graphics/src/ShaderResources.cpp | 4 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 21 ++++++-- .../tests/execution/ImageTable.cpp | 52 +++++++++++++++++++ .../Optimization/ResourceMaterializer.hpp | 1 + .../src/DescriptorBindingBuilder.cpp | 2 +- .../Optimization/src/ResourceMaterializer.cpp | 6 +-- .../Optimization/src/ResourceTracker.cpp | 2 +- docs/dev/debt/shader.md | 2 +- 8 files changed, 77 insertions(+), 13 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 231d471add..a43a347d4a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2435,7 +2435,7 @@ void ShaderResources::buildComplete() { for (auto element = first; element < last; ++element) { const auto index = binding.imageAllocations[element]; if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) images.push_back({samplers[index]->Handle(), VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED}); - else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); + else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL}); else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()}); } if (recorded) runs.emplace_back(write.dstArrayElement, write.descriptorCount); @@ -5211,7 +5211,7 @@ void ShaderResources::resolveImageTables() { storageKeys.push_back(resource.dccAddress); storageFirstLayer.push_back(firstLayer); storageWritten.push_back(true); - storageAtomic.push_back(false); + storageAtomic.push_back(element < binding.imageAtomic.size() && binding.imageAtomic[element]); storageAtomic64.push_back(false); describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast(resource.tileMode), resource.dccAddress, true}); bindings[storageBinding].imageAllocations[absolute] = storageTextures.size() - 1u; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 18e07d3fcd..df69092486 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1339,6 +1339,7 @@ constexpr std::uint32_t TableFormat8888UNorm = 56; constexpr std::uint32_t TableFormat32UInt = 20; constexpr std::uint32_t TableFormat16SInt = 12; constexpr std::uint32_t TableFormat11_11_10UInt = 34; +constexpr std::uint32_t TableFormat32_32UInt = 62; constexpr std::uint32_t TableType2D = 9; constexpr std::uint32_t TableType3D = 10; @@ -2964,7 +2965,17 @@ void verifyImageTableRegistration() { const auto* storeTable = TableBinding(*store.result, DescriptorRole::ImageStorageTable); require((storeFault & Abi::PoisonFlag) == 0u && storeTable != nullptr && storeTable->count != 0u && std::all_of(storeTable->imageWritten.begin(), storeTable->imageWritten.end(), [](bool written) { return written; }), "image table registration: the store's table was not bound as written storage elements"); require(TableBinding(*store.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*store.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: a store-only table bound sampled elements"); - const std::array, 3> unsupportedStores{{{0xf0240f08u, "image_store_mip"}, {0xf0280f08u, "image_store_pck"}, {0xf0440108u, "image_atomic_add"}}}; + auto atomicCode = storeCode; + atomicCode[8] = 0xf0440108u; + const auto atomic = CompileTable(TableRequest(atomicCode, userData)); + const auto& atomicPlan = *atomic.capture->plan; + require(atomicPlan.info.images.size() == 1u && atomicPlan.info.images[0].table == 0u && atomicPlan.info.images[0].tableOperation == TableOperation::Supported, "image table registration: a table T# feeding image_atomic_add was not a supported table operation"); + const auto& atomicMap = TableMap(*atomic.result); + const auto atomicFault = atomicMap[Abi::TableHeader(0) + Abi::TableFault]; + const auto* atomicTable = TableBinding(*atomic.result, DescriptorRole::ImageStorageTable); + require((atomicFault & Abi::PoisonFlag) == 0u && atomicTable != nullptr && atomicTable->count != 0u && atomicTable->imageAtomic.size() == atomicTable->count && std::all_of(atomicTable->imageWritten.begin(), atomicTable->imageWritten.end(), [](bool written) { return written; }) && std::all_of(atomicTable->imageAtomic.begin(), atomicTable->imageAtomic.end(), [](bool atomic) { return atomic; }), "image table registration: the image_atomic_add table was not bound as atomic storage elements"); + require(TableBinding(*atomic.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*atomic.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: an atomic-only table bound sampled elements"); + const std::array, 2> unsupportedStores{{{0xf0240f08u, "image_store_mip"}, {0xf0280f08u, "image_store_pck"}}}; for (const auto& [instruction, name] : unsupportedStores) { auto code = storeCode; code[8] = instruction; @@ -3370,7 +3381,7 @@ void verifySelectedRecordDirectOperations() { FillTableSrt(guest, heap.data(), 32u, 4u); guest.srt[16] = 2u; const auto userData = TableUserData(guest); - const std::array, 4> operations{{{"image_store", 0xf0200f08u, TableFormat32UInt}, {"image_atomic_add", 0xf0440108u, TableFormat32UInt}, {"image_load_pck", 0xf0080f08u, TableFormat32UInt}, {"image_load_by2", 0xf1080308u, TableFormat16UNorm}}}; + const std::array, 5> operations{{{"image_store", 0xf0200f08u, TableFormat32UInt}, {"image_atomic_add", 0xf0440108u, TableFormat32UInt}, {"image_atomic64_add", 0xf0440308u, TableFormat32_32UInt}, {"image_load_pck", 0xf0080f08u, TableFormat32UInt}, {"image_load_by2", 0xf1080308u, TableFormat16UNorm}}}; for (const auto& [name, word0, format] : operations) { const auto what = std::string("selected record direct operations: ") + name; heap[2] = TableT(textures[1].bytes.data(), format, TableType2D); @@ -3378,11 +3389,11 @@ void verifySelectedRecordDirectOperations() { const auto request = TableRequest(code, userData); const auto plan = GetResourcePlan(request); require(plan->info.images.size() == 1u && plan->info.images[0].table == NoTable && !plan->descriptorSources.at(plan->info.images[0].source).tableColumn.has_value(), (what + " through a T# whose record the CPU evaluates was planned through an image table").c_str()); - const auto compiled = CompileTable(request); - require(TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str()); + const auto compiled = word0 == 0xf0440308u ? TableCompiled{} : CompileTable(request); + require(word0 == 0xf0440308u || (TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2]))), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str()); const auto wholeCode = WithImageOperation(TableWholeCode, word0, false); const auto whole = GetResourcePlan(TableRequest(wholeCode, userData)); - require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str()); + require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u || word0 == 0xf0440108u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str()); } const auto mixedCode = WithImageOperation(TableSrtKeyCode, 0xf0200f08u, true); const auto mixed = GetResourcePlan(TableRequest(mixedCode, userData)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp index d353e82b93..d5a89a1586 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp @@ -203,6 +203,22 @@ alignas(256) constexpr std::array StoreLaneTableCode{ 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf0208f08u, 0x00090401u, 0xbf810000u, }; +alignas(256) constexpr std::array AtomicAddLaneTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf0448108u, 0x00090401u, 0xbf810000u, +}; + +alignas(256) constexpr std::array AtomicSwapLaneTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf03c8108u, 0x00090401u, 0xbf810000u, +}; + +alignas(256) constexpr std::array AtomicCmpSwapLaneTableCode{ + 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, + 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0x7e0a0304u, 0x3a080881u, 0xf0408308u, + 0x00090401u, 0xbf810000u, +}; + alignas(256) constexpr std::array StoreTableCutCode{ 0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7da400ffu, 0x00001000u, 0xf0208f08u, 0x00090401u, @@ -1117,6 +1133,41 @@ void RunStoreTests(AgcDriver::VulkanDevice& device) { Require(untouched(), "image table store: a store through a null entry reached an image:\n" + null.log); } +void RunAtomicTests(AgcDriver::VulkanDevice& device) { + struct Registration { + void* data; + Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); } + ~Registration() { GuestAllocations::Mutation().Remove(data); } + } registeredUint(UintTexels.data(), sizeof(UintTexels)), registeredFloat(FloatTexels.data(), sizeof(FloatTexels)), registeredSint(SintTexels.data(), sizeof(SintTexels)); + SetClassRecords(); + for (std::uint32_t x = 0; x < Width; ++x) UintTexels[x] = 0x5000u + x; + for (std::uint32_t x = 0; x < Width; ++x) FloatTexels[x] = 1.0f; + Keys = {}; + Keys[0] = 1; + const auto added = Run(device, AtomicAddLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic test"); + Require(!Faulted(added), "image table atomic: an add through a table faulted:\n" + added.log); + Require(added.imageTableCount != 0u, "image table atomic: the add bound no storage table elements"); + for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == 0x5000u + x + x, "image table atomic: texel " + std::to_string(x) + " of the selected image is " + Hex(UintTexels[x]) + ", expected " + Hex(0x5000u + x + x)); + const auto swapped = Run(device, AtomicSwapLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic test"); + Require(!Faulted(swapped), "image table atomic: an exchange through a table faulted:\n" + swapped.log); + for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == x, "image table atomic: texel " + std::to_string(x) + " of the selected image is " + Hex(UintTexels[x]) + ", expected " + Hex(x)); + const auto compared = Run(device, AtomicCmpSwapLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic test"); + Require(!Faulted(compared), "image table atomic: a compare exchange through a table faulted:\n" + compared.log); + for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == (x ^ 1u), "image table atomic: texel " + std::to_string(x) + " of the selected image is " + Hex(UintTexels[x]) + ", expected " + Hex(x ^ 1u)); + Keys[0] = 8; + const auto addedFloat = Run(device, AtomicAddLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic test"); + Require(!Faulted(addedFloat), "image table atomic: an add on a float record through a table faulted:\n" + addedFloat.log); + for (std::uint32_t x = 0; x < Width; ++x) Require(std::bit_cast(FloatTexels[x]) == 0x3f800000u + x, "image table atomic: float texel " + std::to_string(x) + " is " + Hex(std::bit_cast(FloatTexels[x])) + ", expected " + Hex(0x3f800000u + x)); + Keys[0] = 6; + const auto null = Run(device, AtomicAddLaneTableCode, 1, Lanes, 64); + AgcDriver::Graphics::StorageTexture::FlushAllPending("image table atomic test"); + for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == (x ^ 1u) && std::bit_cast(FloatTexels[x]) == 0x3f800000u + x, "image table atomic: an add through a null entry reached an image:\n" + null.log); +} + void RunSelectedStoreTests(AgcDriver::VulkanDevice& device) { alignas(256) static std::array storeTexels{}; struct Registration { @@ -1509,6 +1560,7 @@ int main() { RunSelectedRecordTests(*device); RunModeTests(*device); RunStoreTests(*device); + RunAtomicTests(*device); RunSelectedStoreTests(*device); RunPointerTests(*device); RunPointerRecordTests(*device); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 1425a5ec58..ae3696883c 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -20,6 +20,7 @@ struct ImageTableElement { bool depthCompare = false; DescriptorBindingKind kind = DescriptorBindingKind::Buffers; std::uint32_t samplers = 0; + bool atomic = false; }; struct ImageTableSamplerElement { diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 51b8c966e9..b3c679dc2e 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -613,7 +613,7 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.imageShapes.push_back(ImageShapeForResource(shape)); physical.imageDepthCompare.push_back(element.depthCompare); physical.imageWritten.push_back(ImageBindingResourceClass(element.kind) == ImageResourceClass::Storage); - physical.imageAtomic.push_back(false); + physical.imageAtomic.push_back(element.atomic); std::uint32_t samplers = 0; for (std::uint32_t sampler = 0; sampler < entry.tableSamplerMasks.size(); ++sampler) { if ((element.samplers & (1u << sampler)) != 0u) samplers |= entry.tableSamplerMasks[sampler]; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 1b15e694b4..8e03103c14 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1713,17 +1713,17 @@ ResolvedImageTables ResourceMaterializer::ResolveImageTables(const ShaderInfo& i auto element = words.dwords; if (mode.conversionFormat != IrBufferFormat::Invalid || mode.depthBits) element[3] = (element[3] & ~0xfffu) | ShaderImageIdentitySwizzle; const auto kind = DescriptorBindingForImage(mode); - const auto hash = hashWords(element, (static_cast(mode.dimension) << 16u) ^ (static_cast(mode.depthCompare) << 8u) ^ static_cast(kind)); + const auto hash = hashWords(element, (static_cast(image.atomic) << 24u) ^ (static_cast(mode.dimension) << 16u) ^ (static_cast(mode.depthCompare) << 8u) ^ static_cast(kind)); auto index = elementIndex.Find(hash, [&](std::uint32_t known) { const auto& entry = out.elements[known]; - return entry.words == element && entry.dimension == mode.dimension && entry.depthCompare == mode.depthCompare && entry.kind == kind; + return entry.words == element && entry.dimension == mode.dimension && entry.depthCompare == mode.depthCompare && entry.kind == kind && entry.atomic == image.atomic; }); if (!index.has_value() && out.elements.size() >= Abi::ImageElementBudget) { record[Abi::ImageRecordFlags] = poison(resource, PoisonReason::Budget, &words); } else { if (!index.has_value()) { index = static_cast(out.elements.size()); - out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u}); + out.elements.push_back({element, mode.dimension, mode.depthCompare, kind, 0u, image.atomic}); elementIndex.Insert(hash, *index); } out.elements[*index].samplers |= directSamplers[resource]; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 0ffeb3be74..48db089c1c 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -1707,7 +1707,7 @@ private: continue; } image.table = next++; - if (image.atomic || image.packed || image.byElements != 0u) { + if (image.atomic64 || image.packed || image.byElements != 0u) { image.tableOperation = TableOperation::Unsupported; } } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index cd42d5cc9f..6d217ba708 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -50,7 +50,7 @@ - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) T# of an MSAA or FMASK image, or of a color format under a depth compare, faults when selected: tables have no multisampled descriptors and no emulated color compare. - Direct [FMASK image loads](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) of 16 samples, CMASK compression (bar the 8-fragment native owner path), packed accesses and other layouts throw. Uncompressed FMASK of up to 8 samples reads a single-mip 2D or 2D-array 8-, 16- or 32-bit `UINT` snapshot with `Z_64KB_X` addressing; selectors may name X, zero or one. `Fmask32_S8_F8` returns the stored word; others expand to 4 bits per sample, an index at or past the fragment count reading 8 (unknown), samples past the count 0. Layouts follow PAL's [expanded values](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9MaskRam.cpp#L5005) and [FMASK surfaces](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/core/addrlib2.cpp#L877). Console results, unused nibbles included, are unmeasured; compressed color sampling is unsupported. - A [DIM 2D read of a 2D MSAA T#](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) returns sample 0, which is not measured on the console's gfx1013, at an implicit LOD above 0, at an explicit one for texels over 16 bits except formats 22 and 56 in SW_64KB_R_X, or for other formats and swizzle modes; 64- and 128-bit SW_64KB_Z_X texels are not covered: gfx1031 does not address their samples. Such a read is an `image_load` without a mip, `image_sample`, `image_sample_lz` or `image_sample_l` of a direct sampled image; other accesses (gathers, bias, gradients, offsets, LOD clamps, depth compares, `image_load_mip`, `image_get_resinfo`, `image_get_lod`, PCK, BY2/BY4) throw, as does a depth-bits T# (16_UINT, 32_UINT or 32_SINT in a Z swizzle mode). On gfx1031 (RX 6700 XT, HIP), such reads return sample 0 at 2x, 4x and 8x for 8- to 128-bit texels in SW_64KB_R_X and 8- to 32-bit ones in SW_64KB_Z_X (formats 1, 5, 7, 11, 13, 14, 18, 22, 29, 56, 60, 64, 71, 77, 133). -- `image_store`, image atomics, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. A T# whose words the SRT walker evaluates is bound directly instead, for every operation of the shader on it (`DirectImageHandles`), so only T#s keyed per lane or by a value the walker cannot evaluate reach a table with these operations; such a direct T# is read when the work is recorded, so a key its own dispatch writes is read before that write. +- `image_store_mip`, `image_store_pck`, PCK2/PCK4 and MIMG BY2/BY4 through an [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) T# fault (`unsupported operation`) when an active lane executes them; the store is dropped and the load reads zeros. 32-bit image atomics through a table lower to `OpAtomic*` on the table `AtomicView` (`Device` scope, relaxed, `AcquireRelease`/`UniformMemory` barrier, as the direct path) instead of faulting; 64-bit atomics still fault (no `Atomic64View` on the table path). - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# with a min or max reduction or FORCE_UNNORMALIZED faults when selected, as does an integer, converted or depth-bits table T# paired with a direct min or max S# that filters between texels. - Uint and Sint [image table](../../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp) entries and direct T#s under a table S# sample through a point-filtered copy of the S#: Vulkan leaves linear filtering of integer formats undefined; the console is assumed to point-sample. - Comparison sampling of a color texture through an [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) S# throws at the first dispatch or draw that binds it. From 0ef31bad752dff3e48dc61da827b5e4e03e29ce1 Mon Sep 17 00:00:00 2001 From: SuarezPM Date: Thu, 22 Oct 2026 07:34:21 -0300 Subject: [PATCH 297/318] fix(shader): emit type-correct zero into table merge phis Table accessors returning scalar uint fed the merge Phi a 4-wide zero, failing SPIR-V validation with SPIRV_TOOLS on. Use ZeroResult like the unsupported path. --- core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index e5c011f423..07ad9585b8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -2041,7 +2041,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { state.tableAccess = {}; if (tables) { EmitLabel(state, invalid); - if (returnsValue) incoming.insert(incoming.end(), {ConstantU32CompositeZero(state, 4u), invalid}); + if (returnsValue) incoming.insert(incoming.end(), {ZeroResult(ctx, inst), invalid}); state.module.AddFunction(spv::OpBranch, merge); } else if (exits) { EmitLabel(state, invalid); @@ -2058,7 +2058,7 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitLabel(state, guardSkip); state.module.AddFunction(spv::OpBranch, guardMerge); EmitLabel(state, guardMerge); - if (returnsValue) state.module.AddFunction(spv::OpPhi, TypeId(state, inst.Type()), guardedId, resultId, merge, ConstantU32CompositeZero(state, 4u), guardSkip); + if (returnsValue) state.module.AddFunction(spv::OpPhi, TypeId(state, inst.Type()), guardedId, resultId, merge, ZeroResult(ctx, inst), guardSkip); } if (returnsValue) ctx.definitions.emplace(&inst, guardedId); } From 31aaf42381d3fe6a53713e380f9c765d8cf1547d Mon Sep 17 00:00:00 2001 From: JAB-dev <68183649+JAB-dev@users.noreply.github.com> Date: Fri, 23 Oct 2026 21:35:16 +0200 Subject: [PATCH 298/318] docs(debt): restore the Packed 16-bit textures entry from main The merge resolution had dropped its storage image sentences, which main keeps and this branch does not change. Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/libSceAgcDriver.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 34bbce8155..05d99dec9e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -202,7 +202,7 @@ - [Layered color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - 3D color targets with 64 KiB render-target or 4 KiB standard tiling render several slices, and so does a `CB_COLOR_VIEW` slice range on a 2D array target through the array's resident image, or on a surface of one slice as an array of the view's last slice + 1 slices, one slice pitch apart: a layered draw into an array without its resident image, a layered draw with a depth target, and color targets of different slice counts throw. The vertex layer export (`PA_CL_VS_OUT_CNTL` USE_VTX_RENDER_TARGET_INDX) reaches the shader only for a layered draw and is otherwise ignored, as before; a layer beyond the view's last slice is undefined in Vulkan, while the console's handling (clamping to `SLICE_MAX`) is not modeled, and a view whose last slice lies past the surface is clamped to its last slice. DCC keys of a 3D color target are taken as one key per 256 bytes over the whole surface, every slice included, and only uniform keys (all uncompressed, or one fast-clear code) are interpreted, as for 2D targets; mixed keys leave the stored texels as they are. The console's DCC key layout for 3D surfaces is not checked (LEGO Horizon Adventures' (PPSA14632) lighting volumes are DCC) - [VGT_TF_PARAM](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - DISTRIBUTION_MODE is taken as scheduling only; the host tessellator decides the order of a patch's triangles. - [Local programs](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) that the hull header leaves at address 0 are prepared at their first draw, one artifact per tessellation configuration, instead of at registration. -- [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. +- [Packed 16-bit textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) - `5_6_5`, `1_5_5_5` and `4_4_4_4` are viewed as R5G6B5, A1R5G5B5 and B4G4R4A4 (the 4-bit format every Vulkan device can sample; R4G4B4A4 is optional and lavapipe lacks it) with a swizzle that reads X from the low bits, and a DST_SEL of the missing W of `5_6_5` reads X. Measured on RDNA2 (gfx1035), not on the console: through `image_load` and `image_sample_lz`, X, Y, Z (and W of `1_5_5_5` and `4_4_4_4`) come from the low, middle and high fields, and W of `5_6_5` returns X (the low 5 bits), not 1. Storage images use compatible R16_UINT views with the [buffer UNORM store conversion](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp); quantization uses nearest even and maps NaN to zero. Rounding ties and NaN handling for packed stores have not been checked on AMD or PS5 hardware. - [Min/max S# reduction](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. - [Min/max S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with a linear mip filter, depth or color-texture compare, or bilinear or anisotropic filtering of an image the recompiler point-samples (sint, converted, depth-bits). With anisotropy the host sampler reduces over its own footprint; [RADV](https://github.com/openbsd/xenocara/blob/1b39033df8d928ec247ca64448c4a63c72302db7/lib/mesa/src/amd/vulkan/radv_sampler.c#L185-L232) programs the same S# for such a Vulkan sampler. Measured on RDNA2 (gfx1030 in [#5156](https://github.com/boykopovar/AnyPS5/pull/5156#issuecomment-6155265616), gfx1035 in #5821), not on the console: without derivatives (`image_sample_lz`), min or max with anisotropic bilinear returns the bilinear minimum or maximum and with anisotropic point the point result, at every ratio field 0 to 4; with derivatives (`image_sample_d`, a 4:1 footprint), anisotropic bilinear at ratio field 2 or 4 reduces over the texels of all its anisotropic taps (the minimum or maximum along the major axis), not one bilinear footprint. A host that is not AMD reduces over its own anisotropic footprint, and Vulkan leaves nearest filtering with anisotropy to the host driver. - [FORCE_DEGAMMA](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp) (libSceAgcDriver) - not measured on the console. A sampler that forces sRGB decoding throws for a format not measured on RDNA2. The rest follows gfx1035 and gfx1031 (point and bilinear): sRGB textures sample as without the bit, as in shadPS4 (PS4); UNORM `8`, `8_8`, `8_8_8_8`, BC1, BC2, BC3 and BC7 sample through their sRGB view (decoded before filtering), and SNORM `8`, `8_8` and `8_8_8_8` through the sRGB view of their bytes, as gfx1031 reads them; `16`, `16_16` and `16_16_16_16` UNORM, SNORM and FLOAT, `32`, `32_32` and `32_32_32_32` FLOAT, `10_11_11`, `2_10_10_10`, `5_9_9_9`, `5_6_5`, `1_5_5_5`, `4_4_4_4`, BC4, BC5 and BC6H sample as without the bit, as gfx1031 does. From 9796a23046bd3306290da6d3f0cf9d5b9e38d178 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:18:22 +0000 Subject: [PATCH 299/318] fix(shader): copy only what the SRT evaluator reads of plan branch conditions ExtractPlan copied the whole argument closure of every control-flow condition into the plan. The draw-time walk (SrtDescriptorEvaluation) uses a condition only when the evaluator computes it, and an operation outside the evaluator's set ends the evaluation without reading its arguments. For conditions on vector loads, image results or values carried around loops the copy reached most of the program, and none of it was ever read. Detail::Evaluator::Reads names what the evaluator reads of a value: nothing (an operation it does not evaluate, an immediate), its arguments (the operations it evaluates, identities, phis, and the pairs CompositeExtract reads through), or its whole closure (a raw read, whose handle LoadedFromMemory walks). EvaluateInst returns early for a value it reads nothing of, so the two cannot drift apart. ownPlanValues still copies descriptor sources, SRT reads and fill values whole, and copies a condition only as far as Reads goes: an operation it does not read is copied without its arguments. The pure and guarded flat slots are computed before the copy, from the same values as before. Evidence: a title that registers about 5,700 shaders at load held 24.1 M copied IR values in its plans (about 5.4 GiB by value size), of which 2.1 M were reachable from descriptor sources, SRT reads and fills (counted by a local report over the recompiler's source map). Tested: agc_shader_memory gains verifyPlanConditionPruning: a condition on a load with a 64-addition address keeps the load as a leaf and none of the additions, and conditions read through an extracted pair and through a raw read still evaluate. It fails without the change. Full ctest passes. Resolved onto main: Reads is a static member of BasicEvaluator next to MayEvaluate, and its operation list is main's evaluator's (evaluatedArguments adds BitCount32, FindILsb32 and FindUMsb32; the undefined values, which main never evaluates, read nothing). The EvaluateInst guard reads the node's value, so it covers the compact plans too. ownPlanValues now runs after the flat slots and before the compact plan, and gives a value it keeps without arguments an empty operand list, so the leaf drops the operand capacity main's IrValue constructor reserves. Main's MayEvaluate already drops the condition on the vector load, so the test checks that it is gone. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 76 +++++++++++++++++++ .../Optimization/SrtWalker/SrtEvaluator.hpp | 7 ++ .../Optimization/src/ResourceMaterializer.cpp | 35 ++++++--- .../src/SrtWalker/SrtEvaluator.cpp | 29 +++++++ 4 files changed, 135 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 18e07d3fcd..10701d0bb6 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -567,6 +567,81 @@ void verifyRegisterSources() { require(!EquivalentValue(plan, &samplerRegister, &firstVector), "different register types were merged"); } +void verifyPlanConditionPruning() { + using namespace ShaderRecompiler; + std::vector> values; + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + values.push_back(std::make_unique(opcode, type, ids++)); + return *values.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + const auto equal = [&](IrValue& left, IrValue& right) -> IrValue& { + auto& compare = make(IrOpcode::IEqual32, IrType::Bool); + compare.AddArgument(&left); + compare.AddArgument(&right); + return compare; + }; + constexpr std::uint32_t Additions = 64; + IrValue* address = &userData(0); + for (std::uint32_t index = 0; index < Additions; ++index) { + auto& sum = make(IrOpcode::IAdd32, IrType::U32); + sum.AddArgument(address); + sum.AddArgument(&constant(4)); + address = ∑ + } + auto& load = make(IrOpcode::LoadBufferU32, IrType::U32); + load.AddArgument(address); + auto& pair = make(IrOpcode::CompositeConstructU32x2, IrType::U32x2); + pair.AddArgument(&userData(1)); + pair.AddArgument(&load); + auto& low = make(IrOpcode::CompositeExtractU32x2, IrType::U32); + low.AddArgument(&pair); + low.AddArgument(&constant(0)); + static std::uint32_t word = 5; + const auto wordAddress = static_cast(reinterpret_cast(&word)); + auto& handle = make(IrOpcode::CompositeConstructU64, IrType::U64); + handle.AddArgument(&userData(2)); + handle.AddArgument(&userData(3)); + auto& read = make(IrOpcode::LoadAddressU32, IrType::U32); + read.SetFlags(MemoryFlags{0, 0}); + read.AddArgument(&handle); + read.AddArgument(&constant(0)); + IrProgram program; + auto& resources = program.Resources(); + resources.srtPlanComplete = true; + resources.resourceTrackingComplete = true; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + resources.memoryInfo.push_back(memory); + resources.controlFlow.push_back({&equal(load, constant(0)), {1, 2}, {}, {}}); + resources.controlFlow.push_back({&equal(low, constant(7)), {3, 4}, {}, {}}); + resources.controlFlow.push_back({&equal(read, constant(5)), {5, 6}, {}, {}}); + const auto plan = ResourceMaterializer{}.ExtractPlan(program); + const auto copied = std::ranges::count_if(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::IAdd32; }); + require(copied == 0, "plan pruning: the address computation under a load the evaluator cannot read was copied"); + const auto copiedLoad = std::ranges::find_if(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::LoadBufferU32; }); + require(copiedLoad != plan.valueStorage.end() && copiedLoad->ArgumentCount() == 0u, "plan pruning: the unevaluable load was not kept as a leaf"); + const std::array data{0x100u, 7u, static_cast(wordAddress), static_cast(wordAddress >> 32u)}; + SrtRuntime runtime{data}; + Detail::Evaluator evaluator(plan, runtime); + std::uint32_t result = 0; + require(plan.controlFlow[0].condition == nullptr, "plan pruning: a condition on a vector load was kept"); + require(evaluator.Evaluate(Detail::IrNode(plan.controlFlow[1].condition), result) && result == 1u, "plan pruning: a condition read through an extracted pair was not evaluated"); + require(evaluator.Evaluate(Detail::IrNode(plan.controlFlow[2].condition), result) && result == 1u, "plan pruning: a condition on a raw read lost its handle"); +} + void verifySignedMinimum() { using namespace ShaderRecompiler; IrResourcePlan plan; @@ -8054,6 +8129,7 @@ int main(int argc, char** argv) { verifySignedMinimum(); verifyMayEvaluate(); verifyPureFlatSlots(); + verifyPlanConditionPruning(); verifyGuardedDescriptorSources(); verifyLaneMaskGuardedDescriptorSources(); verifySrtTape(); diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp index 2fa129cb49..776a3c6dd9 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtEvaluator.hpp @@ -74,6 +74,12 @@ struct InaccessibleRead { std::uint64_t address = 0; }; +enum class EvaluatorReads : std::uint8_t { + Nothing, + Arguments, + Closure, +}; + class DenseValues { public: struct Slot { @@ -174,6 +180,7 @@ public: static bool MayEvaluate(const IrResourcePlan& program, IrValue* value, const IrValue* activeMask = nullptr); static bool MayDecide(const IrResourcePlan& program, IrValue* value); + static EvaluatorReads Reads(const IrResourcePlan& program, const IrValue& value); bool Evaluate(TNode value, std::uint32_t& result); bool EvaluateWide(TNode value, std::uint64_t& result); void ReportInaccessibleReads(InaccessibleRead* sink) { _inaccessible = sink; } diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 1b15e694b4..e2cc0cca5b 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -2204,22 +2205,31 @@ void ownPlanValues(IrResourcePlan& plan) { for (auto& dword : source.dwords) roots.push_back(&dword); } for (auto& read : plan.srtReads) roots.push_back(&read.value); - for (auto& block : plan.controlFlow) roots.push_back(&block.condition); for (auto& value : plan.uniformFill.values) roots.push_back(&value); + const auto closures = roots.size(); + for (auto& block : plan.controlFlow) roots.push_back(&block.condition); std::unordered_map clones; + std::unordered_set closed; + std::unordered_set opened; std::vector order; - std::vector pending; - for (const auto* root : roots) { - if (*root != nullptr) pending.push_back(*root); + std::vector> pending; + for (std::size_t index = 0; index < roots.size(); ++index) { + if (*roots[index] != nullptr) pending.emplace_back(*roots[index], index < closures); } while (!pending.empty()) { - const auto* value = pending.back(); + const auto [value, closure] = pending.back(); pending.pop_back(); - if (!clones.emplace(value, nullptr).second) continue; - order.push_back(value); + if (clones.emplace(value, nullptr).second) order.push_back(value); + const auto reads = closure ? Detail::EvaluatorReads::Closure : Detail::Evaluator::Reads(plan, *value); + if (reads == Detail::EvaluatorReads::Nothing || closed.contains(value)) continue; + if (reads == Detail::EvaluatorReads::Closure) { + closed.insert(value); + } else if (!opened.insert(value).second) { + continue; + } for (const auto* argument : value->Arguments()) { - if (argument != nullptr) pending.push_back(argument); + if (argument != nullptr) pending.emplace_back(argument, reads == Detail::EvaluatorReads::Closure); } } plan.valueStorage.Reserve(order.size()); @@ -2240,11 +2250,12 @@ void ownPlanValues(IrResourcePlan& plan) { return clone; }; for (const auto* value : order) { + const auto count = closed.contains(value) || opened.contains(value) ? value->ArgumentCount() : 0u; std::vector arguments; std::vector phiBlocks; - arguments.reserve(value->ArgumentCount()); - if (value->IsPhi()) phiBlocks.reserve(value->ArgumentCount()); - for (std::size_t index = 0; index < value->ArgumentCount(); index++) { + arguments.reserve(count); + if (value->IsPhi()) phiBlocks.reserve(count); + for (std::size_t index = 0; index < count; index++) { const auto* argument = value->Argument(index); arguments.push_back(argument == nullptr ? nullptr : clones.at(argument)); if (value->IsPhi()) phiBlocks.push_back(blockFor(value->PhiBlock(index))); @@ -2336,7 +2347,6 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const for (auto& block : plan.controlFlow) { if (block.condition != nullptr && !Detail::Evaluator::MayDecide(source, block.condition)) block.condition = nullptr; } - ownPlanValues(plan); const auto addSource = [&plan](std::uint32_t index) { if (index >= plan.descriptorSources.size()) { throw std::runtime_error("ResourceMaterializer::ExtractPlan resource references an unknown descriptor source"); @@ -2363,6 +2373,7 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const } plan.pureFlatSlots = Detail::ComputePureFlatSlots(plan); plan.guardedSrtSlots = Detail::ComputeGuardedFlatSlots(plan); + ownPlanValues(plan); buildCompactPlan(plan); return plan; } diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp index 1412970d44..7b8536caac 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp @@ -463,8 +463,37 @@ bool BasicEvaluator::MayDecide(const IrResourcePlan& program, IrValue* va } } +template +EvaluatorReads BasicEvaluator::Reads(const IrResourcePlan& program, const IrValue& value) { + if (value.IsIdentity()) { + return EvaluatorReads::Arguments; + } + if (value.HasImmediate()) { + return EvaluatorReads::Nothing; + } + switch (value.Opcode()) { + case IrOpcode::LoadAddressU32: + case IrOpcode::ReadConstBuffer: return IsRawRead(program, value) ? EvaluatorReads::Closure : EvaluatorReads::Nothing; + case IrOpcode::CompositeConstructU32x2: + case IrOpcode::IAddCarry32: + case IrOpcode::GetUserData: + case IrOpcode::GetShaderBase: + case IrOpcode::Phi: + case IrOpcode::ReadFirstLane: + case IrOpcode::BitCastU32F32: + case IrOpcode::BitCastF32U32: + case IrOpcode::CompositeExtractU64: + case IrOpcode::CompositeExtractU32x2: + case IrOpcode::ReadConst: return EvaluatorReads::Arguments; + default: return evaluatedArguments(value.Opcode()) ? EvaluatorReads::Arguments : EvaluatorReads::Nothing; + } +} + template bool BasicEvaluator::EvaluateInst(const TNode& inst, std::uint64_t& result) { + if (Reads(_program, *inst.Value()) == EvaluatorReads::Nothing) { + return false; + } std::uint64_t a = 0; std::uint64_t b = 0; std::uint64_t c = 0; From 402f891787d79783b26ade2a43b053450419a495 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:13:24 +0000 Subject: [PATCH 300/318] fix(shader): keep only what a resource plan's conditions can evaluate A resource plan owns copies of the IR values its run-time evaluation reads: the descriptor sources, the SRT reads, the uniform fills, and the branch conditions that tell which images a dispatch can reach. Below a scalar load from memory, the copy took every value the load depends on, whether the evaluator can compute it or not. A condition that reads memory after vector work (a load predicated by a vector compare, an address computed from vector data) brought the program's whole vector graph into the plan: in a title's programs 2.4 million of 2.6 million retained values, about 500 MiB, belonged to twelve or so conditions per program. Below a condition, the copy now follows only what the evaluator reads: the arguments of values it can compute, and for a load from memory the words of its descriptor and its offset; a value the evaluator cannot compute is kept without its arguments, which is what evaluating it needs (it does not evaluate). Descriptor sources keep their whole closure, which the device image pre-pass walks. Tested with a new test (agc_driver_resource_plan_retention): the plan of a program whose branch condition reads a scalar load at an address taken from vector data now keeps the same number of values for vector chains of 1, 16 and 64 steps before the load (84, 114 and 210 before); the device table, written image, buffer descriptor, memory and SRT range and shader disk cache tests pass; in a title's corpus the plans of 314 programs hold 40 thousand values instead of 837 thousand; the corpus recompile with SPIR-V validation has no invalid module. Resolved onto main: the retention test runs on DeviceTableFixture.hpp, which main does not have, so the test and its CMake line are left out here; the next commits test this on main. --- .../Optimization/src/ResourceMaterializer.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index e2cc0cca5b..3e50e96a8d 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -2223,13 +2223,21 @@ void ownPlanValues(IrResourcePlan& plan) { if (clones.emplace(value, nullptr).second) order.push_back(value); const auto reads = closure ? Detail::EvaluatorReads::Closure : Detail::Evaluator::Reads(plan, *value); if (reads == Detail::EvaluatorReads::Nothing || closed.contains(value)) continue; - if (reads == Detail::EvaluatorReads::Closure) { + if (closure) { closed.insert(value); } else if (!opened.insert(value).second) { continue; } for (const auto* argument : value->Arguments()) { - if (argument != nullptr) pending.emplace_back(argument, reads == Detail::EvaluatorReads::Closure); + if (argument != nullptr) pending.emplace_back(argument, closure); + } + if (!closure && reads == Detail::EvaluatorReads::Closure && value->ArgumentCount() != 0u && value->Argument(0) != nullptr) { + const auto* handle = value->Argument(0)->Resolve(); + if (opened.insert(handle).second) { + for (const auto* argument : handle->Arguments()) { + if (argument != nullptr) pending.emplace_back(argument, false); + } + } } } plan.valueStorage.Reserve(order.size()); From d257635baee3d123112fce2430520a70fd4d3a8b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:17:01 +0000 Subject: [PATCH 301/318] test(shader): check what a resource plan keeps of its branch conditions The plan of a hand-built program keeps exactly the 40 values its two branch conditions and two descriptor sources read: none of the 64 additions under the vector load that the conditions reach only through operations the evaluator does not read, which the whole copy kept (170 values). Its walk decides both conditions and resolves both sources as the program's own walk does. The plan of a compute program whose branch condition reads a scalar load at an address taken from vector data keeps the same number of values for vector chains of 1, 16 and 64 steps before the load. This is fable-void's agc_driver_resource_plan_retention from cca1f5a8d, which runs on his DeviceTableFixture; here the program is built from raw words and read through GetResourcePlan. The pruning case also takes the checks of his cb13fecfe: a condition on a select of a vector load is dropped (main's MayEvaluate drops it), the readable conditions stay, and no plan value keeps use lists or spare operand capacity. Co-authored-by: fablevoid <340827264+fable-void@users.noreply.github.com> --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 127 +++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 10701d0bb6..27bca762d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -628,20 +628,143 @@ void verifyPlanConditionPruning() { resources.controlFlow.push_back({&equal(load, constant(0)), {1, 2}, {}, {}}); resources.controlFlow.push_back({&equal(low, constant(7)), {3, 4}, {}, {}}); resources.controlFlow.push_back({&equal(read, constant(5)), {5, 6}, {}, {}}); + auto& choice = make(IrOpcode::SelectU32, IrType::U32); + choice.AddArgument(&equal(userData(4), constant(0))); + choice.AddArgument(&load); + choice.AddArgument(&constant(1)); + resources.controlFlow.push_back({&equal(choice, constant(1)), {7, 8}, {}, {}}); const auto plan = ResourceMaterializer{}.ExtractPlan(program); const auto copied = std::ranges::count_if(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::IAdd32; }); require(copied == 0, "plan pruning: the address computation under a load the evaluator cannot read was copied"); + require(plan.controlFlow[0].condition == nullptr && plan.controlFlow[3].condition == nullptr, "plan pruning: a condition the evaluator can never read was kept"); + require(plan.controlFlow[1].condition != nullptr && plan.controlFlow[2].condition != nullptr, "plan pruning: a condition the evaluator can read was dropped"); const auto copiedLoad = std::ranges::find_if(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::LoadBufferU32; }); require(copiedLoad != plan.valueStorage.end() && copiedLoad->ArgumentCount() == 0u, "plan pruning: the unevaluable load was not kept as a leaf"); + require(std::ranges::all_of(plan.valueStorage, [](const IrValue& value) { return !value.HasUses() && value.OperandUses().empty() && value.Arguments().capacity() == value.ArgumentCount(); }), "plan compaction: a plan value keeps use lists or spare operand capacity"); const std::array data{0x100u, 7u, static_cast(wordAddress), static_cast(wordAddress >> 32u)}; SrtRuntime runtime{data}; Detail::Evaluator evaluator(plan, runtime); std::uint32_t result = 0; - require(plan.controlFlow[0].condition == nullptr, "plan pruning: a condition on a vector load was kept"); require(evaluator.Evaluate(Detail::IrNode(plan.controlFlow[1].condition), result) && result == 1u, "plan pruning: a condition read through an extracted pair was not evaluated"); require(evaluator.Evaluate(Detail::IrNode(plan.controlFlow[2].condition), result) && result == 1u, "plan pruning: a condition on a raw read lost its handle"); } +struct PlanWalk { + std::vector results; + std::vector flat; + std::vector active; +}; + +PlanWalk walkPlan(const ShaderRecompiler::IrResourcePlan& plan, std::span sources, std::span cleanFlatSlots, const ShaderRecompiler::SrtRuntime& runtime) { + PlanWalk walk; + ShaderRecompiler::SrtWalker{}.EvaluateRuntimeSources(plan, sources, runtime, walk.results, walk.flat, cleanFlatSlots, walk.active); + return walk; +} + +bool readFive(void*, std::uint64_t, std::uint32_t* value) { + *value = 5u; + return true; +} + +void verifyPlanRetention() { + using namespace ShaderRecompiler; + std::vector> values; + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + values.push_back(std::make_unique(opcode, type, ids++)); + return *values.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + const auto binary = [&](IrOpcode opcode, IrType type, IrValue& left, IrValue& right) -> IrValue& { + auto& value = make(opcode, type); + value.AddArgument(&left); + value.AddArgument(&right); + return value; + }; + constexpr std::uint32_t Chain = 64; + IrValue* address = &userData(4); + for (std::uint32_t step = 0; step < Chain; ++step) address = &binary(IrOpcode::IAdd32, IrType::U32, *address, constant(1)); + auto& load = make(IrOpcode::LoadBufferU32, IrType::U32); + load.AddArgument(address); + auto& low = binary(IrOpcode::CompositeExtractU32x2, IrType::U32, binary(IrOpcode::CompositeConstructU32x2, IrType::U32x2, userData(1), load), constant(0)); + auto& mask = binary(IrOpcode::IEqual32, IrType::Bool, userData(5), constant(0)); + auto& choice = make(IrOpcode::SelectU32, IrType::U32); + choice.AddArgument(&mask); + choice.AddArgument(&userData(6)); + choice.AddArgument(&load); + auto& offset = binary(IrOpcode::ReadFirstLane, IrType::U32, choice, mask); + auto& read = binary(IrOpcode::LoadAddressU32, IrType::U32, binary(IrOpcode::CompositeConstructU64, IrType::U64, userData(2), userData(3)), offset); + read.SetFlags(MemoryFlags{0, 0}); + IrProgram program; + auto& resources = program.Resources(); + resources.srtPlanComplete = true; + resources.resourceTrackingComplete = true; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + resources.memoryInfo.push_back(memory); + for (std::uint32_t source = 0; source < 2u; ++source) { + DescriptorSource descriptor; + descriptor.dwordCount = 4; + for (std::uint32_t dword = 0; dword < 4u; ++dword) descriptor.dwords[dword] = &userData(10u + source * 4u + dword); + resources.descriptorSources.push_back(descriptor); + BufferResource buffer; + buffer.source = source; + resources.info.buffers.push_back(buffer); + } + resources.controlFlow.push_back({&binary(IrOpcode::IEqual32, IrType::Bool, low, constant(7)), {1, 2}, {0}, {}}); + resources.controlFlow.push_back({&binary(IrOpcode::IEqual32, IrType::Bool, read, constant(5)), {3, 4}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {1}, {}}); + const auto plan = ResourceMaterializer{}.ExtractPlan(program); + constexpr std::size_t Read = 40; + require(plan.valueStorage.Size() == Read, ("plan retention: the plan keeps " + std::to_string(plan.valueStorage.Size()) + " values; its conditions and descriptor sources read " + std::to_string(Read)).c_str()); + require(std::ranges::none_of(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::IAdd32; }), "plan retention: the address under a vector load was copied"); + const auto copiedLoad = std::ranges::find_if(plan.valueStorage, [](const IrValue& value) { return value.Opcode() == IrOpcode::LoadBufferU32; }); + require(copiedLoad != plan.valueStorage.end() && copiedLoad->ArgumentCount() == 0u, "plan retention: the vector load was not kept as a leaf"); + std::array userWords{}; + userWords[1] = 7u; + userWords[2] = 0x1000u; + userWords[6] = 8u; + for (std::uint32_t word = 10; word < userWords.size(); ++word) userWords[word] = 0x100u + word; + SrtRuntime runtime{userWords}; + runtime.readMemory = readFive; + runtime.readSpecializationMemory = readFive; + const auto copiedWalk = walkPlan(plan, plan.materializationSources, plan.cleanFlatSlots, runtime); + const std::array sources{0u, 1u}; + const auto wholeWalk = walkPlan(program.Resources(), sources, {}, runtime); + require(wholeWalk.active == std::vector{1u, 0u}, "plan retention: the program's walk did not decide both conditions"); + require(copiedWalk.active == wholeWalk.active && copiedWalk.results == wholeWalk.results && copiedWalk.flat == wholeWalk.flat, "plan retention: the plan's walk differs from the program's"); +} + +void verifyPlanRetentionChains() { + using namespace ShaderRecompiler; + std::vector kept; + std::vector conditions; + for (const std::uint32_t chain : {1u, 16u, 64u}) { + std::vector code{0xf4080600u, 0xfa000060u, 0xbf8cc07fu, 0x7e140300u}; + code.insert(code.end(), chain, 0x4a141481u); + code.insert(code.end(), {0x34141482u, 0xe0301000u, 0x80060b0au, 0xbf8c0000u, 0x7e28050bu, 0xf4000540u, 0x28000000u, 0xf4080700u, 0xfa000020u, 0xbf8c0000u, 0xbf078015u, 0xbf840002u, 0xe0700000u, 0x80070000u, 0xbf810000u}); + const std::array userData{0x10000u, 0u}; + const auto plan = GetResourcePlan(FrontendRequest(code, userData)); + kept.push_back(plan->valueStorage.Size()); + conditions.push_back(static_cast(std::ranges::count_if(plan->controlFlow, [](const ResourceBlock& block) { return block.condition != nullptr; }))); + } + require(conditions.front() != 0u, "plan retention: the program's plan keeps no branch condition, so the test does not reach the condition's values"); + require(kept[0] == kept[1] && kept[1] == kept[2], ("plan retention: the plan keeps " + std::to_string(kept[0]) + ", " + std::to_string(kept[1]) + " and " + std::to_string(kept[2]) + " values for vector chains of 1, 16 and 64 steps before the scalar load its branch condition reads; it must keep only what the condition's evaluation can reach").c_str()); +} + void verifySignedMinimum() { using namespace ShaderRecompiler; IrResourcePlan plan; @@ -8130,6 +8253,8 @@ int main(int argc, char** argv) { verifyMayEvaluate(); verifyPureFlatSlots(); verifyPlanConditionPruning(); + verifyPlanRetention(); + verifyPlanRetentionChains(); verifyGuardedDescriptorSources(); verifyLaneMaskGuardedDescriptorSources(); verifySrtTape(); From 6f36f29ea364b8ed615eef41ba7d924765ea5056 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 03:18:46 +0000 Subject: [PATCH 302/318] fix(shader): answer phis and null bases in a trimmed plan as the program does A plan copies a branch condition only as far as the evaluator reads it, and an operation the evaluator does not read is kept without its arguments. Two questions the walk asks still read below such a leaf. ResolveInvariantPhi compares a phi's incoming values with EquivalentValue, which follows their arguments, so two leaves with the same operation looked equal in the copy where their arguments differ in the program: a phi whose incoming values differ only below a vector load resolved in the plan and not in the program, and the plan decided a condition the program leaves open. LoadedFromMemory looks for a read from memory anywhere below a raw read's handle, so a handle that reaches memory only below a leaf lost it in the copy: a read from a null base counted as a null root, and its condition stayed open where the program decides it. ownPlanValues now checks both after it copies. Each phi it copied in part must resolve in the copy to the copy of what it resolves to in the program, and each raw read's handle must be loaded from memory in the copy when it is in the program. A phi that differs is copied whole; a handle that differs keeps the arguments along its shortest path to a read from memory (its whole closure if that path is already kept), and the copy is made again. The checks only add values, so the loop ends, at worst with the whole copy. IsMemoryRead names the operations that LoadedFromMemory and the path both stop at. Tested: agc_shader_memory's verifyPlanCopyAnswers walks a phi whose incoming values differ only below a vector load, and a read from a null base whose handle reaches memory only through a select arm the active mask never takes, through the plan's tape, the plan's interpreter and the program's own walk, and all three agree. Without the checks the plan decides the first condition and leaves the second open. --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 94 ++++++++++ .../SrtWalker/SrtInstructionPredicates.hpp | 1 + .../Optimization/src/ResourceMaterializer.cpp | 177 ++++++++++++------ .../SrtWalker/SrtInstructionPredicates.cpp | 7 +- 4 files changed, 219 insertions(+), 60 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 27bca762d5..e1248cd723 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -765,6 +765,99 @@ void verifyPlanRetentionChains() { require(kept[0] == kept[1] && kept[1] == kept[2], ("plan retention: the plan keeps " + std::to_string(kept[0]) + ", " + std::to_string(kept[1]) + " and " + std::to_string(kept[2]) + " values for vector chains of 1, 16 and 64 steps before the scalar load its branch condition reads; it must keep only what the condition's evaluation can reach").c_str()); } +void verifyPlanCopyAnswers() { + using namespace ShaderRecompiler; + std::vector> values; + std::vector> blocks; + std::uint32_t ids = 0; + const auto make = [&](IrOpcode opcode, IrType type) -> IrValue& { + values.push_back(std::make_unique(opcode, type, ids++)); + return *values.back(); + }; + const auto constant = [&](std::uint32_t value) -> IrValue& { + auto& immediate = make(IrOpcode::Void, IrType::U32); + immediate.SetImmediateU32(value); + return immediate; + }; + const auto userData = [&](std::uint32_t index) -> IrValue& { + auto& reg = make(IrOpcode::Void, IrType::ScalarReg); + reg.SetRegister({RegisterBank::Scalar, index}); + auto& read = make(IrOpcode::GetUserData, IrType::U32); + read.AddArgument(®); + return read; + }; + const auto binary = [&](IrOpcode opcode, IrType type, IrValue& left, IrValue& right) -> IrValue& { + auto& value = make(opcode, type); + value.AddArgument(&left); + value.AddArgument(&right); + return value; + }; + const auto vectorLoad = [&](IrValue& address) -> IrValue& { + auto& load = make(IrOpcode::LoadBufferU32, IrType::U32); + load.AddArgument(&address); + return load; + }; + const auto rawRead = [&](IrValue& low, IrValue& high, IrValue& offset) -> IrValue& { + auto& read = binary(IrOpcode::LoadAddressU32, IrType::U32, binary(IrOpcode::CompositeConstructU64, IrType::U64, low, high), offset); + read.SetFlags(MemoryFlags{0, 0}); + return read; + }; + const auto walks = [&](IrProgram& program, IrValue& read, const std::array& userWords, std::uint8_t expected, const char* what) { + auto& resources = program.Resources(); + resources.srtPlanComplete = true; + resources.resourceTrackingComplete = true; + MemoryInfo memory; + memory.kind = ResourceKind::ScalarAddress; + resources.memoryInfo.push_back(memory); + DescriptorSource descriptor; + descriptor.dwordCount = 4; + for (std::uint32_t dword = 0; dword < 4u; ++dword) descriptor.dwords[dword] = &userData(10u + dword); + resources.descriptorSources.push_back(descriptor); + BufferResource buffer; + resources.info.buffers.push_back(buffer); + resources.controlFlow.push_back({&binary(IrOpcode::IEqual32, IrType::Bool, read, constant(5)), {1, 2}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {}, {}}); + resources.controlFlow.push_back({nullptr, {}, {0}, {}}); + const auto plan = ResourceMaterializer{}.ExtractPlan(program); + SrtRuntime runtime{userWords}; + runtime.readMemory = readFive; + runtime.readSpecializationMemory = readFive; + const std::array sources{0u}; + const auto whole = walkPlan(resources, sources, {}, runtime); + const auto taped = walkPlan(plan, plan.materializationSources, plan.cleanFlatSlots, runtime); + const auto interpreted = walkPlan(plan, sources, {}, runtime); + require(whole.active == std::vector{expected}, what); + require(taped.active == whole.active && taped.results == whole.results, what); + require(interpreted.active == whole.active && interpreted.results == whole.results, what); + }; + std::array userWords{}; + for (std::uint32_t word = 10; word < userWords.size(); ++word) userWords[word] = 0x100u + word; + + IrProgram phiProgram; + auto& shared = userData(1); + const auto low = [&](IrValue& hidden) -> IrValue& { + return binary(IrOpcode::CompositeExtractU32x2, IrType::U32, binary(IrOpcode::CompositeConstructU32x2, IrType::U32x2, shared, hidden), constant(0)); + }; + auto& left = *blocks.emplace_back(std::make_unique(1u)); + auto& right = *blocks.emplace_back(std::make_unique(2u)); + auto& phi = make(IrOpcode::Phi, IrType::U32); + phi.AddPhiOperand(&left, &low(vectorLoad(userData(4)))); + phi.AddPhiOperand(&right, &low(vectorLoad(userData(5)))); + userWords[1] = 8u; + userWords[2] = 0x1000u; + walks(phiProgram, rawRead(userData(2), userData(3), phi), userWords, 1u, "plan copy: a phi whose incoming values differ below a vector load resolved in the plan, not in the program"); + + IrProgram rootProgram; + auto& mask = binary(IrOpcode::IEqual32, IrType::Bool, userData(5), constant(0)); + auto& choice = make(IrOpcode::SelectU32, IrType::U32); + choice.AddArgument(&mask); + choice.AddArgument(&userData(2)); + choice.AddArgument(&vectorLoad(rawRead(userData(8), userData(9), constant(0)))); + userWords[1] = 0u; + userWords[2] = 0u; + walks(rootProgram, rawRead(binary(IrOpcode::ReadFirstLane, IrType::U32, choice, mask), userData(3), constant(0)), userWords, 0u, "plan copy: a read from a null base loaded from memory only below a vector load counted as a null root in the plan, not in the program"); +} + void verifySignedMinimum() { using namespace ShaderRecompiler; IrResourcePlan plan; @@ -8255,6 +8348,7 @@ int main(int argc, char** argv) { verifyPlanConditionPruning(); verifyPlanRetention(); verifyPlanRetentionChains(); + verifyPlanCopyAnswers(); verifyGuardedDescriptorSources(); verifyLaneMaskGuardedDescriptorSources(); verifySrtTape(); diff --git a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp index fa1310cff9..34a381669e 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/SrtWalker/SrtInstructionPredicates.hpp @@ -10,6 +10,7 @@ namespace ShaderRecompiler::Detail { bool IsRawRead(const IrResourcePlan& program, const IrValue& inst); bool IsRawRead(const IrResourcePlan& program, IrOpcode op, std::uint32_t memoryIndex); +bool IsMemoryRead(IrOpcode opcode); bool LoadedFromMemory(const IrValue& handle); bool IsDescriptorHandle(IrOpcode opcode); bool IsRuntimeSelect(IrOpcode opcode); diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index 3e50e96a8d..5387447c2d 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1,6 +1,7 @@ #include "Optimization/ResourceMaterializer.hpp" #include "Optimization/SrtWalker/SrtFlatSlotClasses.hpp" #include "Optimization/SrtWalker/SrtEvaluator.hpp" +#include "Optimization/SrtWalker/SrtInstructionPredicates.hpp" #include "Optimization/ShaderStageInputInfo.hpp" #include "RdnaDecoder/RdnaDescriptorFormat.hpp" #include "RdnaDecoder/RdnaImageOpDecoder.hpp" @@ -2199,6 +2200,27 @@ void ResourceMaterializer::PrepareImageModes(ShaderInfo& info) { namespace { +std::vector pathToMemoryRead(const IrValue& handle) { + std::unordered_map parents; + std::vector queue; + const auto visit = [&](const IrValue* argument, const IrValue* parent) { + if (argument == nullptr) return; + argument = argument->Resolve(); + if (!argument->HasImmediate() && parents.emplace(argument, parent).second) queue.push_back(argument); + }; + for (const auto* argument : handle.Arguments()) visit(argument, nullptr); + for (std::size_t index = 0; index < queue.size(); ++index) { + const auto* value = queue[index]; + if (Detail::IsMemoryRead(value->Opcode())) { + std::vector path; + for (const auto* step = parents.at(value); step != nullptr; step = parents.at(step)) path.push_back(step); + return path; + } + for (const auto* argument : value->Arguments()) visit(argument, value); + } + return {}; +} + void ownPlanValues(IrResourcePlan& plan) { std::vector roots; for (auto& source : plan.descriptorSources) { @@ -2209,71 +2231,110 @@ void ownPlanValues(IrResourcePlan& plan) { const auto closures = roots.size(); for (auto& block : plan.controlFlow) roots.push_back(&block.condition); - std::unordered_map clones; - std::unordered_set closed; - std::unordered_set opened; - std::vector order; - std::vector> pending; - for (std::size_t index = 0; index < roots.size(); ++index) { - if (*roots[index] != nullptr) pending.emplace_back(*roots[index], index < closures); - } - while (!pending.empty()) { - const auto [value, closure] = pending.back(); - pending.pop_back(); - if (clones.emplace(value, nullptr).second) order.push_back(value); - const auto reads = closure ? Detail::EvaluatorReads::Closure : Detail::Evaluator::Reads(plan, *value); - if (reads == Detail::EvaluatorReads::Nothing || closed.contains(value)) continue; - if (closure) { - closed.insert(value); - } else if (!opened.insert(value).second) { - continue; + std::unordered_set whole; + std::unordered_set linked; + for (;;) { + std::unordered_map clones; + std::unordered_set closed; + std::unordered_set opened; + std::unordered_set handles; + std::vector phis; + std::vector order; + std::vector> pending; + for (std::size_t index = 0; index < roots.size(); ++index) { + if (*roots[index] != nullptr) pending.emplace_back(*roots[index], index < closures); } - for (const auto* argument : value->Arguments()) { - if (argument != nullptr) pending.emplace_back(argument, closure); - } - if (!closure && reads == Detail::EvaluatorReads::Closure && value->ArgumentCount() != 0u && value->Argument(0) != nullptr) { - const auto* handle = value->Argument(0)->Resolve(); - if (opened.insert(handle).second) { - for (const auto* argument : handle->Arguments()) { - if (argument != nullptr) pending.emplace_back(argument, false); + while (!pending.empty()) { + const auto value = pending.back().first; + const auto closure = pending.back().second || whole.contains(value); + pending.pop_back(); + if (clones.emplace(value, nullptr).second) order.push_back(value); + auto reads = closure ? Detail::EvaluatorReads::Closure : Detail::Evaluator::Reads(plan, *value); + if (reads == Detail::EvaluatorReads::Nothing && linked.contains(value)) reads = Detail::EvaluatorReads::Arguments; + if (reads == Detail::EvaluatorReads::Nothing || closed.contains(value)) continue; + if (closure) { + closed.insert(value); + } else if (!opened.insert(value).second) { + continue; + } else if (value->IsPhi()) { + phis.push_back(value); + } + for (const auto* argument : value->Arguments()) { + if (argument != nullptr) pending.emplace_back(argument, closure); + } + if (!closure && reads == Detail::EvaluatorReads::Closure && value->ArgumentCount() != 0u && value->Argument(0) != nullptr) { + const auto* handle = value->Argument(0)->Resolve(); + handles.insert(handle); + if (opened.insert(handle).second) { + for (const auto* argument : handle->Arguments()) { + if (argument != nullptr) pending.emplace_back(argument, false); + } } } } - } - plan.valueStorage.Reserve(order.size()); - for (const auto* value : order) { - auto* clone = plan.valueStorage.Create(value->Opcode(), value->Type(), static_cast(plan.valueStorage.Size())); - clone->SetFlags(value->Flags()); - if (value->HasImmediate()) clone->SetImmediateU64(value->ImmediateU64()); - clone->SetRegister(value->Register()); - clones[value] = clone; - } - std::unordered_map blocks; - const auto blockFor = [&](const IrBlock* block) { - auto& clone = blocks[block]; - if (clone == nullptr) { - plan.blockStorage.push_back(std::make_unique(block->Id())); - clone = plan.blockStorage.back().get(); + IrValueArena storage; + storage.Reserve(order.size()); + for (const auto* value : order) { + auto* clone = storage.Create(value->Opcode(), value->Type(), static_cast(storage.Size())); + clone->SetFlags(value->Flags()); + if (value->HasImmediate()) clone->SetImmediateU64(value->ImmediateU64()); + clone->SetRegister(value->Register()); + clones[value] = clone; } - return clone; - }; - for (const auto* value : order) { - const auto count = closed.contains(value) || opened.contains(value) ? value->ArgumentCount() : 0u; - std::vector arguments; - std::vector phiBlocks; - arguments.reserve(count); - if (value->IsPhi()) phiBlocks.reserve(count); - for (std::size_t index = 0; index < count; index++) { - const auto* argument = value->Argument(index); - arguments.push_back(argument == nullptr ? nullptr : clones.at(argument)); - if (value->IsPhi()) phiBlocks.push_back(blockFor(value->PhiBlock(index))); + std::vector> blockStorage; + std::unordered_map blocks; + const auto blockFor = [&](const IrBlock* block) { + auto& clone = blocks[block]; + if (clone == nullptr) { + blockStorage.push_back(std::make_unique(block->Id())); + clone = blockStorage.back().get(); + } + return clone; + }; + for (const auto* value : order) { + const auto count = closed.contains(value) || opened.contains(value) ? value->ArgumentCount() : 0u; + std::vector arguments; + std::vector phiBlocks; + arguments.reserve(count); + if (value->IsPhi()) phiBlocks.reserve(count); + for (std::size_t index = 0; index < count; index++) { + const auto* argument = value->Argument(index); + arguments.push_back(argument == nullptr ? nullptr : clones.at(argument)); + if (value->IsPhi()) phiBlocks.push_back(blockFor(value->PhiBlock(index))); + } + clones.at(value)->SetDetachedOperands(std::move(arguments), std::move(phiBlocks)); } - clones.at(value)->SetDetachedOperands(std::move(arguments), std::move(phiBlocks)); + const auto resolvesAlike = [&](const IrValue* phi) { + try { + const auto* expected = ResolveInvariantPhi(plan, const_cast(phi)); + const auto* copied = ResolveInvariantPhi(plan, clones.at(phi)); + if (expected == nullptr) return copied == nullptr; + const auto found = clones.find(expected); + return found != clones.end() && found->second == copied; + } catch (const std::exception&) { + return false; + } + }; + bool grown = false; + for (const auto* phi : phis) { + if (!closed.contains(phi) && !resolvesAlike(phi)) grown = whole.insert(phi).second || grown; + } + for (const auto* handle : handles) { + if (closed.contains(handle) || Detail::LoadedFromMemory(*clones.at(handle))) continue; + const auto path = pathToMemoryRead(*handle); + bool linkedStep = false; + for (const auto* step : path) linkedStep = linked.insert(step).second || linkedStep; + if (!path.empty()) grown = (linkedStep || whole.insert(handle).second) || grown; + } + if (grown) continue; + plan.valueStorage = std::move(storage); + plan.blockStorage = std::move(blockStorage); + for (auto* root : roots) { + if (*root != nullptr) *root = clones.at(*root); + } + plan.denseValueIds = true; + return; } - for (auto* root : roots) { - if (*root != nullptr) *root = clones.at(*root); - } - plan.denseValueIds = true; } void buildCompactPlan(IrResourcePlan& plan) { diff --git a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp index 855fd960af..140e1e7a2b 100644 --- a/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp +++ b/core/shader/recompiler/Optimization/src/SrtWalker/SrtInstructionPredicates.cpp @@ -20,6 +20,10 @@ bool IsRawRead(const IrResourcePlan& program, IrOpcode op, std::uint32_t index) return (op == IrOpcode::LoadAddressU32 && kind == ResourceKind::ScalarAddress) || (op == IrOpcode::ReadConstBuffer && kind == ResourceKind::ScalarBuffer); } +bool IsMemoryRead(IrOpcode opcode) { + return opcode == IrOpcode::ReadConst || opcode == IrOpcode::LoadAddressU32 || opcode == IrOpcode::ReadConstBuffer; +} + bool LoadedFromMemory(const IrValue& handle) { std::vector pending; std::vector visited; @@ -31,8 +35,7 @@ bool LoadedFromMemory(const IrValue& handle) { value = value->Resolve(); if (value->HasImmediate() || std::find(visited.begin(), visited.end(), value) != visited.end()) continue; visited.push_back(value); - const auto opcode = value->Opcode(); - if (opcode == IrOpcode::ReadConst || opcode == IrOpcode::LoadAddressU32 || opcode == IrOpcode::ReadConstBuffer) return true; + if (IsMemoryRead(value->Opcode())) return true; for (std::size_t index = 0; index < value->ArgumentCount(); ++index) pending.push_back(value->Argument(index)); } return false; From 193b11c32fdcfcad45a88924e83610a3a34de020 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sun, 18 Oct 2026 15:39:43 +0300 Subject: [PATCH 303/318] fix(agc): reuse pipeline cache headers on long Windows paths --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 ++ .../prx/libSceAgcDriver/tests/Recorder.cpp | 74 +++++++++++++++++++ core/shader/recompiler/CMakeLists.txt | 1 + 3 files changed, 81 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 75f7e48d00..948ed262c6 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1515,6 +1515,12 @@ if(BUILD_TESTING) endif() add_test(NAME agc_pipeline_cache COMMAND agc_driver_recorder_tests --pipeline-cache-only) add_test(NAME agc_pipeline_cache_file COMMAND agc_driver_recorder_tests --pipeline-cache-file-only) + add_test(NAME agc_pipeline_cache_header COMMAND agc_driver_recorder_tests --pipeline-cache-header-only) + set_tests_properties(agc_pipeline_cache_header PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77) + if(WIN32) + set_tests_properties(agc_pipeline_cache_header PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_pipeline_library COMMAND agc_driver_recorder_tests --pipeline-library-only) add_test(NAME agc_pipeline_library_off COMMAND agc_driver_recorder_tests --pipeline-library-only) set_tests_properties(agc_pipeline_library agc_pipeline_library_off PROPERTIES SKIP_RETURN_CODE 77) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index cf83ce9e23..870ed541d2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -31,6 +31,7 @@ #include "ResidentPresent.hpp" #include "ImageTableAbi.hpp" #include "HostImportLifetime.hpp" +#include "ShaderCacheDirectory.hpp" #include "SampleLod_spv.h" #include "BufferUpdate_spv.h" #include "SampleArray_spv.h" @@ -9741,6 +9742,74 @@ void pipelineCacheTests(const Device& device, bool benchmark) { std::cout << "Pipeline cache key and retained lifetime tests passed\n"; } +struct PipelineCacheFileProbe { + Context& context; + PFN_vkGetDeviceProcAddr resolver; + std::atomic creations{0}; + static inline PipelineCacheFileProbe* active = nullptr; + explicit PipelineCacheFileProbe(Context& context) : context(context), resolver(context.deviceProc) { + Require(active == nullptr, "pipeline cache file probe is already active"); + active = this; + context.deviceProc = resolve; + } + ~PipelineCacheFileProbe() { + context.deviceProc = resolver; + active = nullptr; + } + static VKAPI_ATTR VkResult VKAPI_CALL createCache(VkDevice device, const VkPipelineCacheCreateInfo* info, const VkAllocationCallbacks* callbacks, VkPipelineCache* created) { + ++active->creations; + return reinterpret_cast(active->resolver(device, "vkCreatePipelineCache"))(device, info, callbacks, created); + } + static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) { + if (std::strcmp(name, "vkCreatePipelineCache") == 0) return reinterpret_cast(createCache); + return active->resolver(device, name); + } +}; + +void pipelineCacheHeaderTests(const Device& device) { + const auto root = std::filesystem::temp_directory_path() / ("aps5-pipeline-header-test-" + std::to_string(std::random_device{}())); + Require(root.native().size() < 225, "the pipeline cache header test requires a temporary root shorter than 225 characters"); +#ifdef _WIN32 + std::error_code cleanupError; + const auto cleanupRoot = ShaderRecompiler::NativeCachePath_nid_no_patch(root, cleanupError); + Require(!cleanupError, "the pipeline cache header test could not resolve its cleanup directory"); +#else + const auto& cleanupRoot = root; +#endif + struct RemoveDirectory { + std::filesystem::path directory; + ~RemoveDirectory() { + std::error_code error; + std::filesystem::remove_all(directory, error); + } + } removeDirectory{cleanupRoot}; + auto directory = root; + while (directory.native().size() < 220) directory /= "long-pipeline-cache-directory"; +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); + _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "0", 1); + setenv("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str(), 1); +#endif + auto context = device.GetContext(); + PipelineCacheFileProbe probe(context); + PipelineCache cache(context, device.Properties()); + cache.SaveNow(); + std::vector files; + for (const auto& entry : std::filesystem::directory_iterator(directory)) files.push_back(entry.path()); + Require(files.size() == 1 && files.front().filename().string().starts_with("vk-pipelines-"), "the long pipeline cache directory does not hold exactly one cache file"); + Require(files.front().native().size() > 260, "the pipeline cache header test did not exceed MAX_PATH"); + std::vector stored; + Require(ShaderRecompiler::ReadWholeFile(files.front(), stored) && !stored.empty(), "the long pipeline cache file was not persisted"); + const auto creations = probe.creations.load(); + for (int repeat = 0; repeat < 3; ++repeat) cache.SaveNow(); + Require(probe.creations.load() == creations, "an unchanged long-path pipeline cache was reloaded for an idle save"); + std::vector after; + Require(ShaderRecompiler::ReadWholeFile(files.front(), after) && after == stored, "an idle save changed the long pipeline cache file"); + std::cout << "long-path pipeline cache header reused without reloading\n"; +} + int pipelineCacheFileTests(const Device& device) { const auto& context = device.GetContext(); const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-cache-file-test-" + std::to_string(std::random_device{}())); @@ -10790,6 +10859,11 @@ int main(int argc, char** argv) { return pipelineCacheFileTests(device); } + if (argc == 2 && std::string_view(argv[1]) == "--pipeline-cache-header-only") { + pipelineCacheHeaderTests(device); + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--descriptor-pool-only") { descriptorPoolTests(device); return 0; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 07dee3a0d9..a9dd8b251f 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -259,6 +259,7 @@ function(add_shader_recompiler target runtimeTarget) RayTracingStrict RayTracingMiss ReadWholeFile + NativeCachePath_nid_no_patch Recompile RecompileCacheKey::ContextHash RecompileResult From 19d4bee8231e464de8432b882a0c43a07eea2606 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:22:20 -0300 Subject: [PATCH 304/318] test(agc): bound cache directory and check unread --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 870ed541d2..3a7bcffbc0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -9784,7 +9784,12 @@ void pipelineCacheHeaderTests(const Device& device) { } } removeDirectory{cleanupRoot}; auto directory = root; - while (directory.native().size() < 220) directory /= "long-pipeline-cache-directory"; + while (directory.native().size() < 220) { + const auto remaining = 245 > directory.native().size() ? 245 - directory.native().size() : 1; + const auto chunk = std::min(remaining, 16); + directory /= std::string(chunk, 'd'); + } + Require(directory.native().size() >= 220 && directory.native().size() <= 249, "the pipeline cache directory is not between 220 and 249 characters"); #ifdef _WIN32 _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); @@ -9807,6 +9812,19 @@ void pipelineCacheHeaderTests(const Device& device) { Require(probe.creations.load() == creations, "an unchanged long-path pipeline cache was reloaded for an idle save"); std::vector after; Require(ShaderRecompiler::ReadWholeFile(files.front(), after) && after == stored, "an idle save changed the long pipeline cache file"); +#ifdef _WIN32 + std::error_code lockError; + const auto lockNative = ShaderRecompiler::NativeCachePath_nid_no_patch(files.front(), lockError); + Require(!lockError, "could not resolve long cache path"); + const HANDLE lockHandle = CreateFileW(lockNative.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (lockHandle != INVALID_HANDLE_VALUE) { + PipelineCache lockedCache(context, device.Properties()); + lockedCache.SaveNow(); + CloseHandle(lockHandle); + std::vector retained; + Require(ShaderRecompiler::ReadWholeFile(files.front(), retained) && retained == stored, "an unreadable long pipeline cache file was overwritten"); + } +#endif std::cout << "long-path pipeline cache header reused without reloading\n"; } From 48d526af6af517c7c23d323ad7730174f3749fc7 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:41:02 -0300 Subject: [PATCH 305/318] test(agc): require lock and verify unreadable cache diagnostic --- .../prx/libSceAgcDriver/tests/Recorder.cpp | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 3a7bcffbc0..5c589b3b87 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -9816,14 +9816,27 @@ void pipelineCacheHeaderTests(const Device& device) { std::error_code lockError; const auto lockNative = ShaderRecompiler::NativeCachePath_nid_no_patch(files.front(), lockError); Require(!lockError, "could not resolve long cache path"); - const HANDLE lockHandle = CreateFileW(lockNative.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); - if (lockHandle != INVALID_HANDLE_VALUE) { + struct FileLockGuard { + HANDLE handle = INVALID_HANDLE_VALUE; + ~FileLockGuard() { + if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); + } + } lockGuard{CreateFileW(lockNative.c_str(), GENERIC_READ, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; + Require(lockGuard.handle != INVALID_HANDLE_VALUE, "could not lock pipeline cache file"); + std::string capturedStderr; + { + StderrCapture capture; PipelineCache lockedCache(context, device.Properties()); lockedCache.SaveNow(); - CloseHandle(lockHandle); - std::vector retained; - Require(ShaderRecompiler::ReadWholeFile(files.front(), retained) && retained == stored, "an unreadable long pipeline cache file was overwritten"); + capturedStderr = capture.Finish(); } + Require(capturedStderr.find("cannot read") != std::string::npos, "an unreadable long pipeline cache file did not report the cannot read diagnostic"); + if (lockGuard.handle != INVALID_HANDLE_VALUE) { + CloseHandle(lockGuard.handle); + lockGuard.handle = INVALID_HANDLE_VALUE; + } + std::vector retained; + Require(ShaderRecompiler::ReadWholeFile(files.front(), retained) && retained == stored, "an unreadable long pipeline cache file was overwritten"); #endif std::cout << "long-path pipeline cache header reused without reloading\n"; } From a06df95447629c31241245e0d4deb3b4e7389625 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 06:36:34 -0300 Subject: [PATCH 306/318] test(agc): follow versioned cache directories --- core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 5c589b3b87..9e6af9dfd5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -9784,12 +9784,16 @@ void pipelineCacheHeaderTests(const Device& device) { } } removeDirectory{cleanupRoot}; auto directory = root; - while (directory.native().size() < 220) { - const auto remaining = 245 > directory.native().size() ? 245 - directory.native().size() : 1; + char version[17]; + std::snprintf(version, sizeof(version), "%016llx", static_cast(ShaderRecompiler::ShaderDiskCache::Generated::SourceVersion)); + while ((directory / version).native().size() < 220) { + const auto length = (directory / version).native().size(); + const auto remaining = 245 > length ? 245 - length : 1; const auto chunk = std::min(remaining, 16); directory /= std::string(chunk, 'd'); } - Require(directory.native().size() >= 220 && directory.native().size() <= 249, "the pipeline cache directory is not between 220 and 249 characters"); + const auto versionDirectory = directory / version; + Require(versionDirectory.native().size() >= 220 && versionDirectory.native().size() <= 249, "the pipeline cache directory is not between 220 and 249 characters"); #ifdef _WIN32 _putenv_s("ANYPS5_NO_SHADER_CACHE", "0"); _putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str()); @@ -9802,7 +9806,7 @@ void pipelineCacheHeaderTests(const Device& device) { PipelineCache cache(context, device.Properties()); cache.SaveNow(); std::vector files; - for (const auto& entry : std::filesystem::directory_iterator(directory)) files.push_back(entry.path()); + for (const auto& entry : std::filesystem::directory_iterator(versionDirectory)) files.push_back(entry.path()); Require(files.size() == 1 && files.front().filename().string().starts_with("vk-pipelines-"), "the long pipeline cache directory does not hold exactly one cache file"); Require(files.front().native().size() > 260, "the pipeline cache header test did not exceed MAX_PATH"); std::vector stored; From 56ce490ced18f65dd59c154c928bf4e8bab74b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 23 Oct 2026 21:52:18 +0200 Subject: [PATCH 307/318] fix(shader): drop duplicate NativeCachePath_nid_no_patch allowlist entry --- core/shader/recompiler/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index a9dd8b251f..07dee3a0d9 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -259,7 +259,6 @@ function(add_shader_recompiler target runtimeTarget) RayTracingStrict RayTracingMiss ReadWholeFile - NativeCachePath_nid_no_patch Recompile RecompileCacheKey::ContextHash RecompileResult From 30c324a23be4442cd1668af516a0e23ee7d09173 Mon Sep 17 00:00:00 2001 From: battleblob1 Date: Wed, 21 Oct 2026 12:42:26 +0300 Subject: [PATCH 308/318] fix(recompiler): accept EXEC copies in stencil replay masks Since #6291 a pixel shader's EXEC copy is read as the saved word ANDed with WQM(Ballot(true)). MaskReplaySafety only accepts a whole-quad expansion of a ballot it can show excludes helper lanes, and a ballot of true includes them, so a stencil-exporting shader whose own-lane masks come from an EXEC copy was refused (own-lane mask not stable). Every lane of a quad is active (real or helper), so a ballot of true holds the same quad bits as the all-ones mask the proof already treats as quad-stable; it is now classified the same way. Helper-dependent ballots stay uncertain. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/MaskedSelect.cpp | 24 +++++++++++++++++++ core/shader/recompiler/StencilReplay.cpp | 1 + docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp index f4b855ef84..a77f88d58f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/MaskedSelect.cpp @@ -1074,6 +1074,30 @@ int main() { return !StencilReplaySafe(b.program); }); + const auto execCopy = [](Builder& b, IrValue& low, IrValue& high) -> IrValue& { + auto& active = b.emit(IrOpcode::Ballot, IrType::U32x4, {&b.truth()}); + auto& lanes = b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {&b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&active, &b.constant(0u)}), &b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&active, &b.constant(1u)})}); + auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&lanes}); + return b.threadBit(b.wordAnd(low, b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)})), b.wordAnd(high, b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)}))); + }; + + passed &= run("an exec copy masked by the whole-quad active lanes is a stable export guard", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + const auto [low, high] = b.ballotWords(pixelPredicate(b)); + exportUnder(b, execCopy(b, *low, *high), true, true); + return StencilReplaySafe(b.program); + }); + + passed &= run("an exec copy does not hide helper participation in a whole-quad expansion", [&] { + Builder b; + b.program.Resources().stage = IrShaderStage::Pixel; + const auto [low, high] = b.ballotWords(helperPredicate(b, true)); + auto& wqm = b.emit(IrOpcode::WqmU64, IrType::U64, {&b.emit(IrOpcode::CompositeConstructU64, IrType::U64, {low, high})}); + stencilByMask(b, execCopy(b, b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(0u)}), b.emit(IrOpcode::CompositeExtractU64, IrType::U32, {&wqm, &b.constant(1u)}))); + return !StencilReplaySafe(b.program); + }); + passed &= run("only guarded read-only SRT diagnostics can be replayed", [] { Builder b; b.program.Info().usesFaultBuffer = true; diff --git a/core/shader/recompiler/StencilReplay.cpp b/core/shader/recompiler/StencilReplay.cpp index 6ed182542f..30bc43b765 100644 --- a/core/shader/recompiler/StencilReplay.cpp +++ b/core/shader/recompiler/StencilReplay.cpp @@ -270,6 +270,7 @@ private: if (lowBallot && highBallot) { const auto low = onHelper(lowBallot->Argument(0), depth + 1u), high = onHelper(highBallot->Argument(0), depth + 1u); if (low && high && *low == 0u && *high == 0u) result = HelperMask::RealOnly; + else if (Boolean(lowBallot->Argument(0), true) && Boolean(highBallot->Argument(0), true)) result = HelperMask::QuadStable; } else if (mask.low->Opcode() == IrOpcode::BitwiseAnd32 && (waveSize == 32u || mask.high->Opcode() == IrOpcode::BitwiseAnd32)) { const auto pair = [&](unsigned low, unsigned high) { return ReplayMask{mask.low->Argument(low)->Resolve(), waveSize == 32u ? mask.low->Argument(low)->Resolve() : mask.high->Argument(high)->Resolve()}; }; const auto combine = [&](unsigned firstHigh, unsigned secondHigh) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3d7bbfd474..c887053906 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -80,7 +80,7 @@ - [Initial depth and stencil planes](../../../core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp) - the console's reset values of `DB_Z_INFO` and `DB_STENCIL_INFO` (context `0x010`-`0x011`) are not measured. Both start, and return after `CLEAR_STATE`, at zero (formats `INVALID`), the value of Linux's [GFX10 clear-state table](https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/amd/amdgpu/clearstate_gfx10.h). The public [register-default tables](../../../core/libs/prx/libSceAgc/Command/include/RegisterDefaultsData.hpp) write `0x80000180` and `0x20000180` instead, also with formats `INVALID`; of their other bits, only `TILE_STENCIL_DISABLE` would change a result: the HTILE layout of a multisampled depth plane whose `DB_STENCIL_INFO` was never written. - [Type-0 and type-1 texture descriptors](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp) (`TYPE` = 0 or 1 in dword 3) with a nonzero base are bound as null images, like a zero base. TYPE 0 comes from a gfx1036 (RDNA2) measurement in issue #4139 (image sample, load and resinfo return zero, no fault). TYPE 1 comes from a gfx1010 (RDNA1) measurement in #4139 and #5828 (`image_sample_lz`, `image_sample_b`, `image_load` and `image_get_resinfo` return zero, no fault) with format 22 only; TYPE 1 is not measured on RDNA2, with other formats or with depth-compare reads (`image_sample_c*`). Neither type was measured on the console's gfx1013. - [Multisampled color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/MultisampleTarget.cpp) lose their guest-read guards and cannot be reused after a successful allocation removal or unmap covers their full guarded range. The next target, sampled or storage lookup removes retired cache owners; recorded work and sampled views keep their images alive until their last reference is released. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. -- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions, since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. +- [Stencil reference export replay](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) on devices without `VK_EXT_shader_stencil_export` replays the low eight bits of the values proved from constant, select and phi exports, with an export proved on every kept path. An export the prover cannot bound replays all 256 eight-bit values, one pass per value per triangle (logged once per process). It records instances, then their triangles, then the reference classes of each triangle. One pipeline filters fragments at shader termination against a dynamic push value before late stencil and depth tests. The [Vulkan stencil-reference replacement rule](https://docs.vulkan.org/refpages/latest/refpages/source/FragStencilRefEXT.html) discards bits above the attachment's stencil width; supported stencil attachments have eight bits. Numeric subgroup observations throw except bounded own-lane mask plumbing and group control with kept-lane and derivative-helper coverage proved. WQM inputs must exclude helper contributions (a ballot of the active lanes, as in an EXEC copy, covers whole quads and is accepted), since [non-quad subgroup operations may treat helpers as inactive](https://docs.vulkan.org/glslext/latest/glslext/khr/GL_KHR_shader_subgroup.html). Readonly guarded SRT reads may report internal host faults, and read-only BDA reads may report faults; these draws wait and check faults before publishing results, and loops throw. Indirect draws, restart, strips, fans, tessellation, mesh, multisampling, early tests, ordered pixel execution, guest shader memory writes, other fault-producing memory access, clocks, primitive ID inputs, attachment feedback and pipeline face/channel splits throw. An exhausted stage push range throws. Equivalence to console ordering has not been measured on console hardware. - [32-bit depth-bits reads of guest memory](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver): a sampled 32_UINT or 32_SINT T# with a depth swizzle mode that no live depth surface matches reads the guest bytes through an R32_SFLOAT image, as the recompiler's depth-bits read expects, and the shader casts the fetched value back to the stored dword. On RX 6700 XT (gfx1031, AMD Windows driver 32.0.21045.5002) the read keeps float denormals, the sign bit alone and quiet and signaling NaN payloads bit for bit. A host that canonicalizes NaNs or flushes denormals on a 32-bit float fetch would change those dwords, and other drivers are not measured. A live depth surface whose extent or base level differs from the T# is not used; the read takes the guest bytes, as the 32-bit float and 16-bit views already do. - [Reused depth storage](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp): a storage view with another extent or a format that cannot hold the cached depth plane uses guest memory. Depth rendering is not written back to those guest bytes; this admission rule does not prove allocation reuse or preserve a depth writer through an incompatible view. A 2D storage view, or a one-layer 2D array view of layer 0, of level 0 with the plane's extent and format (32-bit float or the 32-bit depth-bits layout for the depth plane, 16-bit unorm for D16, 8-bit uint for the stencil plane) is a storage image that is [filled](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) from the depth image before every use and, when the shader writes it, copied back after the work: a whole-plane copy through a staging buffer each way. A dispatch of the 15-word 8x8x1 image clear kernel ([`MatchStorageImageClear`](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp): one colour read from the V# in `user[8..11]` stored to every texel of the T# in `user[0..7]`) that covers the whole plane and stores 0 or 1 through a 16-bit unorm view, 0 or 0xffff through a 16-bit uint view, or 0 or a normal float in (0, 1] through a 32-bit float or integer view leaves out the copy-in and replaces the copy-out by a clear of the depth image to that value; the dispatch still writes the storage image. The colour is read again when the copy-out is replaced, and a changed value keeps the copy-out. Indirect dispatches, partial last groups, dispatches that queue on the draw pipeline, other values and formats, a plane the kernel covers only partly and stencil planes keep both copies. A written view is also left pending like any other storage image, so the plane's content, drawn depth included, reaches guest memory on the next flush of that range. A 2D array view of more than one slice of the depth plane, or of a later slice, is a storage image over the slices from 0 to its last (`LayeredDepthStorage`); only the slices from its base slice to its last are copied: a slice that has a depth surface at the plane's address plus a whole depth slice per layer (`DepthSliceBytes`, as for the depth plane copies below) is copied from that surface before every use and back to it after the work, every bound slice each time, through a staging buffer; the other slices hold guest memory like any storage image. These images are kept per address, extent, slice count, base slice, format and tile mode until the device goes away, outside the storage cache's budget. Such a view needs a layer pitch equal to that slice size, and another view of the same memory (the 2D view of its first slice, a view from another base slice, or a sampled copy) is a separate image, copied in before and out after each use, so views that are written before a flush and share a slice do not merge: the later copy-out wins, so their results are not merged. A matching view still throws when it is a cube, a 2D array of more than one layer of the stencil plane or from a later slice, or another level, when the surface is multisampled, when it names the depth plane of a 16-bit depth with stencil surface the device stores in a wider format, and when it was written after another view of the same plane (another format, tile mode, address or dimension) replaced it, as two such views bound in one dispatch are. Stored depth values outside 0..1 are not clamped, and Vulkan leaves that copy undefined without `VK_EXT_depth_range_unrestricted`. `agc_driver_depth_storage_dispatch` pins these paths; none is measured on the console. From 0a4027df9a0822a3ab1bf195769e160e2c35d100 Mon Sep 17 00:00:00 2001 From: frankea Date: Wed, 21 Oct 2026 06:56:10 -0400 Subject: [PATCH 309/318] fix(shader): link each table callee next to its own call so large callee sets stay in branch range --- core/libs/tests/ScalarCall.cpp | 40 ++++++++++------ .../ControlFlow/src/GraphBuilder.cpp | 46 ++++++++----------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index 2da9bd35ef..abd7cc3d07 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -271,27 +271,39 @@ static void CheckTableCalls() { const std::vector> callees{{{0x1122334455ull, first}, {0x66778899aaull, second}}}; const auto linkedCalls = LinkCalls(program, {}, {}, calls, callees); const auto& linked = linkedCalls.code; - Require(linkedCalls.fallbacks == std::vector{50u * 4u}); + Require(linkedCalls.fallbacks == std::vector{53u * 4u}); ExpectThrow("is shared with another call", [&] { static_cast(BuildCode(linked, swappc)); }); SwappcInfo linkedSwappc = swappc; linkedSwappc.linkedCallFallbacks = linkedCalls.fallbacks; - Require(linked.size() == 59u && linked[9] == 0xbf820001u && linked[11] == 0xbf850010u && linked[58] == 0xbf9f0000u); - Require(linked[12] == 0xbf07ff0eu && linked[13] == 0x22334455u && linked[14] == 0xbf850004u && linked[15] == 0xbf07ff0fu && linked[16] == 0x11u && linked[17] == 0xbf850001u && linked[18] == 0xbf82001bu); - Require(linked[20] == 0x778899aau && linked[26] == 0xbf078080u && linked[27] == 0xbf820016u && linked[34] == 0xbf068080u && linked[35] == 0xbf82000au && linked[44] == 0xbf068080u && linked[45] == 0xbf820004u); - Require(linked[46] == 0xbb0e0005u && linked[47] == 0xbf82ffdau && linked[48] == 0xbb0e0006u && linked[49] == 0xbf82ffd8u && linked[50] == 0xbe8e210eu && linked[51] == 0xbf82ffd6u); - Require(std::equal(first.begin(), first.end(), linked.begin() + 52) && std::equal(second.begin(), second.end(), linked.begin() + 55)); + Require(linked.size() == 56u && linked[9] == 0xbf820001u && linked[11] == 0xbf850007u && linked[55] == 0xbf9f0000u); + Require(linked[12] == 0xbf07ff0eu && linked[13] == 0x22334455u && linked[14] == 0xbf850010u && linked[15] == 0xbf07ff0fu && linked[16] == 0x11u && linked[17] == 0xbf85000du && linked[18] == 0xbf820007u); + Require(linked[19] == 0xbf07ff0eu && linked[21] == 0xbf850010u && linked[24] == 0xbf85000du && linked[25] == 0xbf068080u && linked[26] == 0xbb0e0001u && linked[27] == 0xbf82ffeeu); + Require(linked[32] == 0x778899aau && linked[33] == 0xbf850010u && linked[37] == 0xbf820007u && linked[40] == 0xbf85000bu && linked[43] == 0xbf850008u && linked[45] == 0xbb0e0001u && linked[46] == 0xbf82ffecu); + Require(linked[50] == 0xbf078080u && linked[51] == 0xbf820001u && linked[52] == 0xbf068080u && linked[53] == 0xbe8e210eu && linked[54] == 0xbf82fff7u); + Require(std::equal(first.begin(), first.end(), linked.begin() + 28) && std::equal(second.begin(), second.end(), linked.begin() + 47)); const auto cfg = BuildCode(linked, linkedSwappc); const auto block = [&](std::uint32_t word) { return cfg.FindBlockByProgramCounter(word * 4u).id; }; - Require(cfg.FindBlockByProgramCounter(46u * 4u).terminator.trueBlock == block(52u)); - Require(cfg.FindBlockByProgramCounter(52u * 4u).terminator.trueBlock == block(47u)); - Require(cfg.FindBlockByProgramCounter(48u * 4u).terminator.trueBlock == block(55u)); - Require(cfg.FindBlockByProgramCounter(55u * 4u).terminator.trueBlock == block(49u)); - Require(cfg.unlinkedCallProgramCounters == std::vector{50u * 4u}); + Require(cfg.FindBlockByProgramCounter(26u * 4u).terminator.trueBlock == block(28u)); + Require(cfg.FindBlockByProgramCounter(28u * 4u).terminator.trueBlock == block(27u)); + Require(cfg.FindBlockByProgramCounter(45u * 4u).terminator.trueBlock == block(47u)); + Require(cfg.FindBlockByProgramCounter(47u * 4u).terminator.trueBlock == block(46u)); + Require(cfg.unlinkedCallProgramCounters == std::vector{53u * 4u}); static_cast(TranslateCode(linked, linkedSwappc)); - std::vector large(0x8000u, 0xbf800000u); - const std::vector> far{{{0x10ull, large}, {0x20ull, first}}}; - ExpectThrow("out of s_call_b64 range", [&] { static_cast(LinkCalls(program, {}, {}, calls, far)); }); + std::vector large(0x3000u, 0xbf800000u); + large.push_back(0xbe80200eu); + const std::vector> far{{{0x10ull, large}, {0x20ull, large}, {0x30ull, large}, {0x40ull, first}}}; + const auto farLinked = LinkCalls(program, {}, {}, calls, far); + const auto farCall = 12u + 3u * (16u + 0x3001u) + 14u; + Require(farLinked.code[farCall] == 0xbb0e0001u && std::equal(first.begin(), first.end(), farLinked.code.begin() + farCall + 2u)); + SwappcInfo farSwappc = swappc; + farSwappc.linkedCallFallbacks = farLinked.fallbacks; + const auto farCfg = BuildCode(farLinked.code, farSwappc); + Require(farCfg.FindBlockByProgramCounter(farCall * 4u).terminator.trueBlock == farCfg.FindBlockByProgramCounter((farCall + 2u) * 4u).id); + Require(farCfg.FindBlockByProgramCounter((farCall + 2u) * 4u).terminator.trueBlock == farCfg.FindBlockByProgramCounter((farCall + 1u) * 4u).id); + std::vector oversized(0x8000u, 0xbf800000u); + const std::vector> overflows{{{0x10ull, oversized}, {0x20ull, first}}}; + ExpectThrow("out of the 16-bit branch range", [&] { static_cast(LinkCalls(program, {}, {}, calls, overflows)); }); const std::vector jumpTable{ 0xbe8c1f00u, 0x800cff0cu, 0x00000058u, 0x820d800du, 0x83928202u, 0xd4c40016u, 0x02000500u, 0x8f128212u, 0xf4000386u, 0x24000000u, diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index eb9e209745..322fae20d5 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -1591,41 +1591,35 @@ LinkedCalls LinkCalls(const RdnaProgram& program, std::span const auto at = "the table callees of the call at program counter " + toHexString(table.programCounter); const auto original = linked[word]; const auto count = callees.size(); - const auto dispatch = linked.size(); - const auto zero = dispatch + 1u; - const auto one = zero + count * 7u + 2u; - const auto calls = one + count * 8u + 2u; - const auto fault = calls + count * 2u; + std::vector segments(count + 1u); + segments[0] = linked.size() + 1u; + for (std::size_t callee = 0; callee < count; ++callee) segments[callee + 1u] = segments[callee] + 16u + callees[callee].code.size(); + const auto one = [&](std::size_t callee) { return segments[callee] + (callee < count ? 7u : 2u); }; + const auto back = [&](std::size_t callee) { return callee == 0u ? word + 1u : segments[callee - 1u] + 15u; }; + const auto fault = segments[count] + 3u; const auto branch = [&](std::uint32_t opcode, std::size_t target) { linked.push_back(branchWord(opcode, linked.size(), target, at.c_str())); }; - linked[word] = branchWord(0xbf820000u, word, dispatch, at.c_str()); - branch(0xbf850000u, one); - for (const bool sccOne : {false, true}) { - for (std::size_t callee = 0; callee < count; ++callee) { - const auto next = linked.size() + (sccOne ? 8u : 7u); + linked[word] = branchWord(0xbf820000u, word, linked.size(), at.c_str()); + branch(0xbf850000u, one(0u)); + for (std::size_t callee = 0; callee < count; ++callee) { + for (const bool sccOne : {false, true}) { + const auto next = sccOne ? one(callee + 1u) : segments[callee + 1u]; linked.insert(linked.end(), {0xbf07ff00u | table.targetRegister, static_cast(callees[callee].target)}); branch(0xbf850000u, next); linked.insert(linked.end(), {0xbf07ff00u | (table.targetRegister + 1u), static_cast(callees[callee].target >> 32u)}); branch(0xbf850000u, next); if (sccOne) linked.push_back(0xbf068080u); - branch(0xbf820000u, calls + callee * 2u); + else branch(0xbf820000u, segments[callee] + 14u); } - linked.push_back(sccOne ? 0xbf068080u : 0xbf078080u); - branch(0xbf820000u, fault); - } - for (std::size_t callee = 0; callee < count; ++callee) { - linked.push_back(0u); - branch(0xbf820000u, word + 1u); - } - fallbacks.push_back(static_cast(linked.size() * 4u)); - linked.push_back(original); - branch(0xbf820000u, word + 1u); - for (std::size_t callee = 0; callee < count; ++callee) { - const auto site = calls + callee * 2u; - const auto offset = static_cast(linked.size()) - static_cast(site) - 1; - if (offset > 0x7fff) throw std::invalid_argument(at + " land out of s_call_b64 range"); - linked[site] = 0xbb000000u | (table.linkRegister << 16u) | static_cast(offset); + linked.push_back(0xbb000001u | (table.linkRegister << 16u)); + branch(0xbf820000u, back(callee)); linked.insert(linked.end(), callees[callee].code.begin(), callees[callee].code.end()); } + linked.push_back(0xbf078080u); + branch(0xbf820000u, fault); + linked.push_back(0xbf068080u); + fallbacks.push_back(static_cast(linked.size() * 4u)); + linked.push_back(original); + branch(0xbf820000u, back(count)); } linked.push_back(0xbf9f0000u); return {std::move(linked), std::move(fallbacks)}; From a1ae72a2e85f7358af3bf8681620540dc696ed86 Mon Sep 17 00:00:00 2001 From: frankea Date: Wed, 21 Oct 2026 07:16:08 -0400 Subject: [PATCH 310/318] fix(shader): a 64-bit SALU result overwrites both dwords of a call link pair --- core/libs/tests/ScalarCall.cpp | 9 +++++++++ core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp | 6 ++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index abd7cc3d07..9cae3fedc9 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -44,6 +44,11 @@ static RdnaInstruction Sop1(std::uint32_t pc, std::uint32_t destination, std::ui return DecodeRdnaSop1(pc, code, 0u); } +static RdnaInstruction Sop2(std::uint32_t pc, std::uint32_t destination, std::uint32_t op, std::uint32_t source0, std::uint32_t source1) { + const std::array code{0x80000000u | (op << 23u) | (destination << 16u) | (source1 << 8u) | source0}; + return DecodeRdnaSop2(pc, code, 0u); +} + static RdnaInstruction Sopp(std::uint32_t pc, std::uint32_t op, std::uint32_t immediate = 0u) { const std::array code{0xbf800000u | (op << 16u) | immediate}; return DecodeRdnaSopp(pc, code, 0u); @@ -318,6 +323,10 @@ static void CheckTableCalls() { ExpectThrow("escapes the constant-offset call/return model", [] { Build({Sop1(0u, 0u, 3u, 8u), Call(4u, 8u, 2u), Sopp(8u, 2u, 0xfffdu), Sopp(12u, 1u), Sop1(16u, 0u, 0x20u, 8u)}); }); + static_cast(Build({Call(0u, 8u, 3u), Sop2(4u, 8u, 0x15u, 0u, 2u), Sop2(8u, 10u, 0x1bu, 8u, 2u), Sopp(12u, 1u), Sop1(16u, 0u, 0x20u, 8u)})); + ExpectThrow("escapes the constant-offset call/return model", [] { + Build({Call(0u, 8u, 3u), Sop2(4u, 8u, 0x0eu, 0u, 2u), Sop2(8u, 10u, 0x1bu, 8u, 2u), Sopp(12u, 1u), Sop1(16u, 0u, 0x20u, 8u)}); + }); } enum class HoistedShape { InLoop, Hoisted, SecondWriterOnArm, LoadOnArmOnly, WriterAfterCall, WriterAfterLoop, CallOnArm, ConstantPointer, SkipsNullTarget, LongBranchWriter }; diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 322fae20d5..71c11f0646 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -503,11 +503,9 @@ bool overwritesScalar(const RdnaInstruction& instruction, std::uint32_t reg, std case RdnaOpcode::SBitset1B64: case RdnaOpcode::SSwappcB64: case RdnaOpcode::SSetpcB64: return false; - case RdnaOpcode::SMovB64: - case RdnaOpcode::SGetpcB64: return first != NoScalarRegister && reg >= first && reg - first < 2u; - default: return first != NoScalarRegister && reg == first; + default: return first != NoScalarRegister && reg >= first && reg - first < std::max(instruction.dataDwordCount, 1u); } - case RdnaInstructionFamily::SOP2: return first != NoScalarRegister && reg == first; + case RdnaInstructionFamily::SOP2: return first != NoScalarRegister && reg >= first && reg - first < std::max(instruction.dataDwordCount, 1u); case RdnaInstructionFamily::SOPK: return instruction.op == RdnaOpcode::SMovkI32 && reg == first; case RdnaInstructionFamily::VOP1: case RdnaInstructionFamily::VOP2: From ef2b164b15be62b13e7d277ecd2bcc545c1409fc Mon Sep 17 00:00:00 2001 From: frankea Date: Wed, 21 Oct 2026 16:41:37 -0400 Subject: [PATCH 311/318] fix(shader): relay later table calls past earlier calls' callee copies --- core/libs/tests/ScalarCall.cpp | 31 +++++++++++++++++++ .../ControlFlow/src/GraphBuilder.cpp | 24 +++++++++++--- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index 9cae3fedc9..0faf175176 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -5,6 +5,7 @@ #include "RdnaDecoder/RdnaMemoryOpDecoder.hpp" #include "ControlFlow/GraphBuilder.hpp" #include +#include #include #include #include @@ -306,6 +307,36 @@ static void CheckTableCalls() { const auto farCfg = BuildCode(farLinked.code, farSwappc); Require(farCfg.FindBlockByProgramCounter(farCall * 4u).terminator.trueBlock == farCfg.FindBlockByProgramCounter((farCall + 2u) * 4u).id); Require(farCfg.FindBlockByProgramCounter((farCall + 2u) * 4u).terminator.trueBlock == farCfg.FindBlockByProgramCounter((farCall + 1u) * 4u).id); + auto twoCallers = caller; + twoCallers.pop_back(); + twoCallers.insert(twoCallers.end(), caller.begin(), caller.end()); + const auto twoProgram = RdnaInstructionDecoder{}.Decode(twoCallers); + auto secondCall = calls[0]; + secondCall.programCounter += 40u; + const std::array twoCalls{calls[0], secondCall}; + const std::vector> twoCallees{far[0], {{0x50ull, first}, {0x60ull, second}}}; + const auto twoLinked = LinkCalls(twoProgram, {}, {}, twoCalls, twoCallees); + const auto& two = twoLinked.code; + const auto follow = [&](std::size_t word, std::size_t& hops) { + hops = 0; + while (two[word] >> 16u == 0xbf82u) { + word = static_cast(static_cast(word) + 1 + static_cast(two[word] & 0xffffu)); + ++hops; + } + return word; + }; + std::size_t hops = 0; + Require(follow(9u, hops) == 23u && hops == 1u && two[21] >> 16u == 0xbf82u && two[22] >> 16u == 0xbf82u); + Require(follow(39u, hops) == 10u && hops == 1u); + const auto secondChain = follow(19u, hops); + Require(hops == 6u && two[secondChain] == 0xbf850007u && two[secondChain + 1u] == 0xbf07ff0eu && two[secondChain + 2u] == 0x50u); + Require(two[secondChain + 15u] == 0xbb0e0001u && std::equal(first.begin(), first.end(), two.begin() + static_cast(secondChain + 17u))); + Require(follow(secondChain + 16u, hops) == 20u && hops == 6u); + SwappcInfo twoSwappc = swappc; + twoSwappc.linkedCallFallbacks = twoLinked.fallbacks; + const auto twoCfg = BuildCode(two, twoSwappc); + Require(twoCfg.FindBlockByProgramCounter(static_cast(secondChain + 15u) * 4u).terminator.trueBlock == twoCfg.FindBlockByProgramCounter(static_cast(secondChain + 17u) * 4u).id); + Require(twoCfg.unlinkedCallProgramCounters.size() == 2u); std::vector oversized(0x8000u, 0xbf800000u); const std::vector> overflows{{{0x10ull, oversized}, {0x20ull, first}}}; ExpectThrow("out of the 16-bit branch range", [&] { static_cast(LinkCalls(program, {}, {}, calls, overflows)); }); diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 71c11f0646..6eb1bae337 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -1582,6 +1582,7 @@ LinkedCalls LinkCalls(const RdnaProgram& program, std::span linked[word] = 0xbb000000u | (userDataCalls[call].linkRegister << 16u) | static_cast(offset); linked.insert(linked.end(), userDataCallees[call].begin(), userDataCallees[call].end()); } + std::vector> stations; for (std::size_t call = 0; call < tableCalls.size(); ++call) { const auto& table = tableCalls[call]; const auto& callees = tableCallees[call]; @@ -1589,14 +1590,28 @@ LinkedCalls LinkCalls(const RdnaProgram& program, std::span const auto at = "the table callees of the call at program counter " + toHexString(table.programCounter); const auto original = linked[word]; const auto count = callees.size(); + const auto relays = 2u * (tableCalls.size() - call - 1u); + std::vector path; + for (const auto& [position, block] : stations) path.push_back(position + 2u * (call - block - 1u)); + const auto station = [&] { + if (relays == 0u) return; + stations.push_back({linked.size(), call}); + linked.insert(linked.end(), relays, 0u); + }; + station(); + const auto start = linked.size(); std::vector segments(count + 1u); - segments[0] = linked.size() + 1u; - for (std::size_t callee = 0; callee < count; ++callee) segments[callee + 1u] = segments[callee] + 16u + callees[callee].code.size(); + segments[0] = start + 1u; + for (std::size_t callee = 0; callee < count; ++callee) segments[callee + 1u] = segments[callee] + 16u + callees[callee].code.size() + relays; const auto one = [&](std::size_t callee) { return segments[callee] + (callee < count ? 7u : 2u); }; - const auto back = [&](std::size_t callee) { return callee == 0u ? word + 1u : segments[callee - 1u] + 15u; }; + const auto back = [&](std::size_t callee) { return callee != 0u ? segments[callee - 1u] + 15u : path.empty() ? word + 1u : path.back() + 1u; }; const auto fault = segments[count] + 3u; const auto branch = [&](std::uint32_t opcode, std::size_t target) { linked.push_back(branchWord(opcode, linked.size(), target, at.c_str())); }; - linked[word] = branchWord(0xbf820000u, word, linked.size(), at.c_str()); + for (std::size_t hop = 0; hop < path.size(); ++hop) { + linked[path[hop]] = branchWord(0xbf820000u, path[hop], hop + 1u < path.size() ? path[hop + 1u] : start, at.c_str()); + linked[path[hop] + 1u] = branchWord(0xbf820000u, path[hop] + 1u, hop == 0u ? word + 1u : path[hop - 1u] + 1u, at.c_str()); + } + linked[word] = branchWord(0xbf820000u, word, path.empty() ? start : path.front(), at.c_str()); branch(0xbf850000u, one(0u)); for (std::size_t callee = 0; callee < count; ++callee) { for (const bool sccOne : {false, true}) { @@ -1611,6 +1626,7 @@ LinkedCalls LinkCalls(const RdnaProgram& program, std::span linked.push_back(0xbb000001u | (table.linkRegister << 16u)); branch(0xbf820000u, back(callee)); linked.insert(linked.end(), callees[callee].code.begin(), callees[callee].code.end()); + station(); } linked.push_back(0xbf078080u); branch(0xbf820000u, fault); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3d7bbfd474..cdd9bf48dc 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -216,7 +216,7 @@ - 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 (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`, or a 4-dword-aligned slice of an `s_load_dwordx8` or `s_load_dwordx16`, 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. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. 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`, or a 4-dword-aligned slice of an `s_load_dwordx8` or `s_load_dwordx16`, 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. The descriptor load may be in an earlier block, such as before a per-lane loop around the call, when it is the only write of the descriptor registers that reaches the buffer load on every path back from it, and no `s_call_b64` or statically resolved call lies on those paths; the paths may run through any `s_swappc_b64` that is not resolved statically, the table call itself included, whose callee is assumed to keep the descriptor registers. The chain compares the target each iteration actually loads, so a callee that changes them can only make a later target miss the chain and fault as an unlinked call. Paths follow fall-through, direct branches and the `s_setpc_b64` jumps whose target the control flow analysis resolves. A path that reaches, before a write of the descriptor registers, the program's first instruction, the entry of an in-program function (an `s_call_b64` target, whose callers are not followed), or an instruction that nothing modelled jumps or falls into fails the search. 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. Each copy follows its own compares and call, so no branch spans more than one copy. A later call's branch to its chain and its return cross earlier calls' copies through a pair of `s_branch` relays before the first of those copies and after each one, so they don't span more than one copy either. 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 call more than 0x7fff dwords before the end of the program and its user-data callees 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. - [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. From b62b28831da14d585d8d2eccc62c5e6cae71582d Mon Sep 17 00:00:00 2001 From: Rookie <175737347+rooklz@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:14:21 +0100 Subject: [PATCH 312/318] feat(libSceCesCs): add the GB18030-2000 and GB18030-2005 profiles Implement sceCesUcsProfileInitGb18030_2000 and sceCesUcsProfileInitGb18030_2005. Two-byte cells are the CP936 table plus the cells that differ, and four-byte sequences are runs over the BMP and the supplementary planes, generated from ICU's gb-18030-2000.ucm (before ICU-8427) and gb-18030-2005.ucm with ICU's GB 18030 ranges. The GB, MBC and MBCS string conversions accept both profiles and produce four-byte UTF-8 and surrogate pairs for supplementary characters. --- core/libs/prx/libSceCesCs/Export.cpp | 225 +++++++++++++--- core/libs/prx/libSceCesCs/Gb18030.hpp | 135 ++++++++++ core/libs/prx/libSceCesCs/PendingExports.cpp | 10 - core/libs/tests/GuestCesChinese.cpp | 260 ++++++++++++++++++- docs/dev/debt/libSceCesCs.md | 5 +- 5 files changed, 583 insertions(+), 52 deletions(-) create mode 100644 core/libs/prx/libSceCesCs/Gb18030.hpp diff --git a/core/libs/prx/libSceCesCs/Export.cpp b/core/libs/prx/libSceCesCs/Export.cpp index 46e3aad1fe..0ba8d0ae27 100644 --- a/core/libs/prx/libSceCesCs/Export.cpp +++ b/core/libs/prx/libSceCesCs/Export.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -12,6 +13,7 @@ #include "JisX0208.hpp" #include "JisX0212.hpp" #include "Cp936.hpp" +#include "Gb18030.hpp" #include "Cp949.hpp" #include "Cp950.hpp" #include "SingleByte.hpp" @@ -141,10 +143,17 @@ uint32_t encodeUtf8(uint32_t code, uint8_t* utf8) { utf8[1] = static_cast(0x80 | (code & 0x3F)); return 2; } - utf8[0] = static_cast(0xE0 | code >> 12); - utf8[1] = static_cast(0x80 | (code >> 6 & 0x3F)); - utf8[2] = static_cast(0x80 | (code & 0x3F)); - return 3; + if (code < 0x10000) { + utf8[0] = static_cast(0xE0 | code >> 12); + utf8[1] = static_cast(0x80 | (code >> 6 & 0x3F)); + utf8[2] = static_cast(0x80 | (code & 0x3F)); + return 3; + } + utf8[0] = static_cast(0xF0 | code >> 18); + utf8[1] = static_cast(0x80 | (code >> 12 & 0x3F)); + utf8[2] = static_cast(0x80 | (code >> 6 & 0x3F)); + utf8[3] = static_cast(0x80 | (code & 0x3F)); + return 4; } constexpr uint8_t EUC_JP_TAG[8] = {'E', 'U', 'C', 'J', 'P', '9', '3', '2'}; @@ -296,6 +305,125 @@ int encodeGb(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& length return CES_ERROR_UNASSIGNED_CODE; } +constexpr uint8_t GB18030_2000_TAG[8] = {'G', 'B', '1', '8', '2', '0', '0', '0'}; +constexpr uint8_t GB18030_2005_TAG[8] = {'G', 'B', '1', '8', '2', '0', '0', '5'}; +constexpr uint32_t GB_CELLS = 126 * 190; + +struct Gb18030Tables { + std::span twoByte; + std::span fourByte; + std::span fallback; +}; + +constexpr Gb18030Tables GB18030_2000{Gb18030::TWO_BYTE_2000, Gb18030::FOUR_BYTE_2000, {}}; +constexpr Gb18030Tables GB18030_2005{Gb18030::TWO_BYTE_2005, Gb18030::FOUR_BYTE_2005, Gb18030::FALLBACK_2005}; + +const Gb18030Tables* gb18030Tables(const void* profile) { + if (profile == nullptr) return nullptr; + if (std::memcmp(profile, GB18030_2000_TAG, sizeof(GB18030_2000_TAG)) == 0) return &GB18030_2000; + if (std::memcmp(profile, GB18030_2005_TAG, sizeof(GB18030_2005_TAG)) == 0) return &GB18030_2005; + return nullptr; +} + +bool isGb18030(const void* profile) { + return gb18030Tables(profile) != nullptr; +} + +bool isGb(const void* profile) { + return isGbk(profile) || isGb18030(profile); +} + +const uint32_t* findRun(std::span runs, uint32_t key) { + const auto next = std::upper_bound(runs.begin(), runs.end(), key, [](uint32_t value, const uint32_t (&run)[3]) { return value < run[0]; }); + if (next == runs.begin()) return nullptr; + const uint32_t* run = *std::prev(next); + return key - run[0] < run[2] ? run : nullptr; +} + +uint32_t gb18030Cell(const Gb18030Tables& tables, uint32_t cell) { + const uint32_t* run = findRun(tables.twoByte, cell); + return run ? run[1] + cell - run[0] : Cp936::TO_UCS2[cell]; +} + +void putGbCell(uint32_t cell, uint8_t* mbcs) { + const uint32_t column = cell % 190; + mbcs[0] = static_cast(0x81 + cell / 190); + mbcs[1] = static_cast(column < 0x3F ? 0x40 + column : 0x41 + column); +} + +int decodeGb18030(const Gb18030Tables& tables, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { + const uint8_t lead = mbcs[0]; + if (lead < 0x80) { + length = 1; + code = lead; + return 0; + } + if (lead == 0x80 || lead == 0xFF) return CES_ERROR_INVALID_ENCODE; + if (mbcsMax < 2) return CES_ERROR_SRC_BUFFER_END; + const uint8_t second = mbcs[1]; + if (second >= 0x30 && second <= 0x39) { + if (mbcsMax < 4) return CES_ERROR_SRC_BUFFER_END; + if (mbcs[2] < 0x81 || mbcs[2] == 0xFF || mbcs[3] < 0x30 || mbcs[3] > 0x39) return CES_ERROR_INVALID_ENCODE; + const uint32_t linear = (((lead - 0x81) * 10 + (second - 0x30)) * 126 + (mbcs[2] - 0x81)) * 10 + (mbcs[3] - 0x30); + const uint32_t* run = findRun(tables.fourByte, linear); + if (!run) return CES_ERROR_UNASSIGNED_CODE; + length = 4; + code = run[1] + linear - run[0]; + return 0; + } + if (second < 0x40 || second == 0x7F || second == 0xFF) return CES_ERROR_INVALID_ENCODE; + length = 2; + code = gb18030Cell(tables, (lead - 0x81) * 190 + (second < 0x7F ? second - 0x40 : second - 0x41)); + return 0; +} + +const std::vector>& gb18030Reverse(const Gb18030Tables& tables) { + const auto build = [](const Gb18030Tables& source) { + std::vector> entries; + entries.reserve(GB_CELLS); + for (uint32_t cell = 0; cell < GB_CELLS; ++cell) entries.emplace_back(gb18030Cell(source, cell), static_cast(cell)); + std::sort(entries.begin(), entries.end()); + return entries; + }; + static const auto reverse2000 = build(GB18030_2000); + static const auto reverse2005 = build(GB18030_2005); + return &tables == &GB18030_2005 ? reverse2005 : reverse2000; +} + +int encodeGb18030(const Gb18030Tables& tables, uint32_t code, uint8_t* mbcs, uint32_t& length) { + if (code < 0x80) { + length = 1; + mbcs[0] = static_cast(code); + return 0; + } + const auto& reverse = gb18030Reverse(tables); + const auto found = std::lower_bound(reverse.begin(), reverse.end(), std::pair(code, 0)); + if (found != reverse.end() && found->first == code) { + putGbCell(found->second, mbcs); + length = 2; + return 0; + } + for (const auto& run : tables.fourByte) { + if (code - run[1] >= run[2]) continue; + uint32_t linear = run[0] + code - run[1]; + mbcs[3] = static_cast(0x30 + linear % 10); + linear /= 10; + mbcs[2] = static_cast(0x81 + linear % 126); + linear /= 126; + mbcs[1] = static_cast(0x30 + linear % 10); + mbcs[0] = static_cast(0x81 + linear / 10); + length = 4; + return 0; + } + for (const auto& entry : tables.fallback) { + if (entry[0] != code) continue; + putGbCell(entry[1], mbcs); + length = 2; + return 0; + } + return CES_ERROR_UNASSIGNED_CODE; +} + bool sjisToJis(uint8_t lead, uint8_t trail, int& row, int& cell) { if (!((lead >= 0x81 && lead <= 0x9F) || (lead >= 0xE0 && lead <= 0xFC))) return false; if (trail < 0x40 || trail == 0x7F || trail > 0xFC) return false; @@ -349,7 +477,7 @@ bool isBig5(const void* profile) { } bool isMbcs(const void* profile) { - return isEucJp(profile) || isSjis(profile) || isSbc(profile) || isEucCn(profile) || isGbk(profile) || isEucKr(profile) || isUhc(profile) || isBig5(profile); + return isEucJp(profile) || isSjis(profile) || isSbc(profile) || isEucCn(profile) || isGb(profile) || isEucKr(profile) || isUhc(profile) || isBig5(profile); } int decodeCp949(bool unified, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { @@ -444,6 +572,7 @@ int encodeDbcs(const std::vector& reverse, bool euc, uint32_t code, ui int decodeMbcs(const void* profile, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t& length, uint32_t& code) { if (isEucKr(profile) || isUhc(profile)) return decodeCp949(isUhc(profile), mbcs, mbcsMax, length, code); if (isBig5(profile)) return decodeCp950(mbcs, mbcsMax, length, code); + if (const Gb18030Tables* tables = gb18030Tables(profile)) return decodeGb18030(*tables, mbcs, mbcsMax, length, code); if (isEucCn(profile) || isGbk(profile)) return decodeGb(profile, mbcs, mbcsMax, length, code); const uint8_t lead = mbcs[0]; if (isSbc(profile)) { @@ -560,6 +689,7 @@ const std::vector& jisReverse(bool strict) { int encodeMbcs(const void* profile, uint32_t code, uint8_t* mbcs, uint32_t& length) { if (isEucKr(profile) || isUhc(profile)) return encodeDbcs(cp949Reverse(), isEucKr(profile), code, mbcs, length); if (isBig5(profile)) return encodeDbcs(cp950Reverse(), false, code, mbcs, length); + if (const Gb18030Tables* tables = gb18030Tables(profile)) return encodeGb18030(*tables, code, mbcs, length); if (isEucCn(profile) || isGbk(profile)) return encodeGb(profile, code, mbcs, length); if (isSbc(profile)) { length = 1; @@ -625,10 +755,21 @@ int decodeUcs2(const uint16_t* ucs2, uint32_t, uint32_t& length, uint32_t& code) } uint32_t encodeUcs2(uint32_t code, uint16_t* ucs2) { + if (code > 0xFFFF) return 0; ucs2[0] = static_cast(code); return 1; } +uint32_t encodeUtf16(uint32_t code, uint16_t* utf16) { + if (code < 0x10000) { + utf16[0] = static_cast(code); + return 1; + } + utf16[0] = static_cast(0xD800 | (code - 0x10000) >> 10); + utf16[1] = static_cast(0xDC00 | (code & 0x3FF)); + return 2; +} + template int toMbcs(const void* context, const Source* source, uint32_t sourceMax, uint32_t* sourceLen, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, Decode decode) { if (!context) return CES_ERROR_INVALID_PARAMETER; @@ -641,7 +782,7 @@ int toMbcs(const void* context, const Source* source, uint32_t sourceMax, uint32 uint32_t length = 0; uint32_t code = 0; if (const int result = decode(source + read, sourceMax - read, length, code); result != 0) return result; - uint8_t encoded[3]; + uint8_t encoded[4]; uint32_t size = 0; if (const int result = encodeMbcs(profile, code, encoded, size); result != 0) return result; if (mbcs) { @@ -670,6 +811,7 @@ int fromMbcs(const void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_ if (const int result = decodeMbcs(profile, mbcs + read, mbcsMax - read, length, code); result != 0) return result; Target encoded[4]; const uint32_t size = encode(code, encoded); + if (size == 0) return CES_ERROR_UNASSIGNED_CODE; if (target) { if (written + size > targetMax) return CES_ERROR_DST_BUFFER_END; std::memcpy(target + written, encoded, size * sizeof(Target)); @@ -690,7 +832,7 @@ int mbcsToUtf8(bool accepted, const void* profile, const uint8_t* mbcs, uint32_t uint32_t consumed = 0; uint32_t code = 0; if (const int result = decodeMbcs(profile, mbcs, mbcsMax, consumed, code); result != 0) return result; - uint8_t encoded[3]; + uint8_t encoded[4]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; std::memcpy(utf8, encoded, produced); @@ -746,6 +888,7 @@ int profiledToUcs2(bool (*matches)(const void*), const void* profile, const uint uint32_t length = 0; uint32_t code = 0; if (const int result = decodeProfiled(matches, profile, source, sourceMax, ucs2, length, code); result != 0) return result; + if (code > 0xFFFF) return CES_ERROR_UNASSIGNED_CODE; *ucs2 = static_cast(code); if (sourceSize) *sourceSize = length; return 0; @@ -755,7 +898,7 @@ int profiledToUtf8(bool (*matches)(const void*), const void* profile, const uint uint32_t length = 0; uint32_t code = 0; if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf8, length, code); result != 0) return result; - uint8_t encoded[3]; + uint8_t encoded[4]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8max < produced) return CES_ERROR_DST_BUFFER_END; std::memcpy(utf8, encoded, produced); @@ -769,10 +912,16 @@ int profiledToUtf16(bool (*matches)(const void*), const void* profile, const uin uint32_t length = 0; uint32_t code = 0; if (const int result = decodeProfiled(matches, profile, source, sourceMax, utf16, length, code); result != 0) return result; - if (utf16max < 1) return CES_ERROR_DST_BUFFER_END; - storeUnit(utf16, code); + const uint32_t units = code < 0x10000 ? 1 : 2; + if (utf16max < units) return CES_ERROR_DST_BUFFER_END; + if (units == 1) { + storeUnit(utf16, code); + } else { + storeUnit(utf16, 0xD800 | (code - 0x10000) >> 10); + storeUnit(utf16 + 1, 0xDC00 | (code & 0x3FF)); + } if (sourceSize) *sourceSize = length; - if (utf16Len) *utf16Len = 1; + if (utf16Len) *utf16Len = units; return 0; } @@ -787,7 +936,7 @@ int profiledToUtf32(bool (*matches)(const void*), const void* profile, const uin } int encodeProfiled(const void* profile, uint32_t code, uint8_t* target, uint32_t targetMax, uint32_t* targetSize) { - uint8_t encoded[3]; + uint8_t encoded[4]; uint32_t size = 0; if (const int result = encodeMbcs(profile, code, encoded, size); result != 0) return result; if (targetMax < size) return CES_ERROR_DST_BUFFER_END; @@ -1132,7 +1281,7 @@ int APS5_VABI sceCesSbcToUtf8(const uint8_t* profile, uint8_t sbc, uint8_t* utf8 if (!isSbc(profile)) return CES_ERROR_INVALID_PARAMETER; if (!utf8) return CES_ERROR_INVALID_DST_BUFFER; - uint8_t encoded[3]; + uint8_t encoded[4]; const uint32_t length = encodeUtf8(sbcToUnicode(profile, sbc), encoded); if (utf8max < length) return CES_ERROR_DST_BUFFER_END; for (uint32_t i = 0; i < length; ++i) utf8[i] = encoded[i]; @@ -1162,7 +1311,7 @@ int APS5_VABI sceCesEucJpToUtf8(const void* profile, const uint8_t* eucBuffer, u uint32_t consumed = 0; uint32_t code = 0; if (const int result = decodeMbcs(profile, eucBuffer, eucMax, consumed, code); result != 0) return result; - uint8_t encoded[3]; + uint8_t encoded[4]; const uint32_t produced = encodeUtf8(code, encoded); if (utf8Max < produced) return CES_ERROR_DST_BUFFER_END; std::memcpy(utf8Buffer, encoded, produced); @@ -1487,11 +1636,11 @@ int APS5_VABI sceCesUtf16StrGetMbcsLen(void* context, const uint16_t* utf16, uin int APS5_VABI sceCesMbcsStrToUtf16Str(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint16_t* utf16, uint32_t utf16max, uint32_t* utf16Len) { if (!utf16) return CES_ERROR_INVALID_DST_BUFFER; - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, utf16, utf16max, utf16Len, encodeUcs2); + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, utf16, utf16max, utf16Len, encodeUtf16); } int APS5_VABI sceCesMbcsStrGetUtf16Len(void* context, const uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen, uint32_t* utf16Len) { - return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf16Len, encodeUcs2); + return fromMbcs(context, mbcs, mbcsMax, mbcsLen, static_cast(nullptr), 0, utf16Len, encodeUtf16); } int APS5_VABI sceCesUtf32StrToMbcsStr(void* context, const uint32_t* utf32, uint32_t utf32max, uint32_t* utf32Len, uint8_t* mbcs, uint32_t mbcsMax, uint32_t* mbcsLen) { @@ -1524,6 +1673,18 @@ void* APS5_VABI sceCesUcsProfileInitGbkCp936(void* sheet) { return sheet; } +void* APS5_VABI sceCesUcsProfileInitGb18030_2000(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, GB18030_2000_TAG, sizeof(GB18030_2000_TAG)); + return sheet; +} + +void* APS5_VABI sceCesUcsProfileInitGb18030_2005(void* sheet) { + if (!sheet) return nullptr; + std::memcpy(sheet, GB18030_2005_TAG, sizeof(GB18030_2005_TAG)); + return sheet; +} + int APS5_VABI sceCesEucCnToUcs2(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2) { return profiledToUcs2(isEucCn, profile, eucBuffer, eucMax, eucSize, ucs2); } @@ -1589,67 +1750,67 @@ int APS5_VABI sceCesUtf32leToEucCn(const uint32_t* utf32, const void* profile, u } int APS5_VABI sceCesGbToUcs2(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* ucs2) { - return profiledToUcs2(isGbk, profile, gbBuffer, gbMax, gbSize, ucs2); + return profiledToUcs2(isGb, profile, gbBuffer, gbMax, gbSize, ucs2); } int APS5_VABI sceCesGbToUtf8(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length) { - return profiledToUtf8(isGbk, profile, gbBuffer, gbMax, gbSize, utf8Buffer, utf8Max, utf8Length); + return profiledToUtf8(isGb, profile, gbBuffer, gbMax, gbSize, utf8Buffer, utf8Max, utf8Length); } int APS5_VABI sceCesGbToUtf16(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return profiledToUtf16(isGbk, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16(isGb, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesGbToUtf16be(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return profiledToUtf16(isGbk, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16(isGb, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesGbToUtf16le(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length) { - return profiledToUtf16(isGbk, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); + return profiledToUtf16(isGb, profile, gbBuffer, gbMax, gbSize, utf16Buffer, utf16Max, utf16Length); } int APS5_VABI sceCesGbToUtf32(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint32_t* utf32) { - return profiledToUtf32(isGbk, profile, gbBuffer, gbMax, gbSize, utf32); + return profiledToUtf32(isGb, profile, gbBuffer, gbMax, gbSize, utf32); } int APS5_VABI sceCesGbToUtf32be(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint32_t* utf32) { - return profiledToUtf32(isGbk, profile, gbBuffer, gbMax, gbSize, utf32); + return profiledToUtf32(isGb, profile, gbBuffer, gbMax, gbSize, utf32); } int APS5_VABI sceCesGbToUtf32le(const void* profile, const uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize, uint32_t* utf32) { - return profiledToUtf32(isGbk, profile, gbBuffer, gbMax, gbSize, utf32); + return profiledToUtf32(isGb, profile, gbBuffer, gbMax, gbSize, utf32); } int APS5_VABI sceCesUcs2ToGb(uint16_t ucs2, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return scalarToProfiled(isGbk, ucs2, profile, gbBuffer, gbMax, gbSize); + return scalarToProfiled(isGb, ucs2, profile, gbBuffer, gbMax, gbSize); } int APS5_VABI sceCesUtf8ToGb(const uint8_t* utf8Buffer, uint32_t utf8Max, uint32_t* utf8Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return bufferToProfiled(isGbk, utf8Buffer, utf8Max, utf8Length, profile, gbBuffer, gbMax, gbSize, decodeUtf8); + return bufferToProfiled(isGb, utf8Buffer, utf8Max, utf8Length, profile, gbBuffer, gbMax, gbSize, decodeUtf8); } int APS5_VABI sceCesUtf16ToGb(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return bufferToProfiled(isGbk, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16); + return bufferToProfiled(isGb, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16); } int APS5_VABI sceCesUtf16beToGb(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return bufferToProfiled(isGbk, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16); + return bufferToProfiled(isGb, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16); } int APS5_VABI sceCesUtf16leToGb(const uint16_t* utf16Buffer, uint32_t utf16Max, uint32_t* utf16Length, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return bufferToProfiled(isGbk, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16); + return bufferToProfiled(isGb, utf16Buffer, utf16Max, utf16Length, profile, gbBuffer, gbMax, gbSize, decodeUtf16); } int APS5_VABI sceCesUtf32ToGb(uint32_t utf32, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return scalarToProfiled(isGbk, utf32, profile, gbBuffer, gbMax, gbSize); + return scalarToProfiled(isGb, utf32, profile, gbBuffer, gbMax, gbSize); } int APS5_VABI sceCesUtf32beToGb(const uint32_t* utf32, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return utf32ToProfiled(isGbk, utf32, profile, gbBuffer, gbMax, gbSize); + return utf32ToProfiled(isGb, utf32, profile, gbBuffer, gbMax, gbSize); } int APS5_VABI sceCesUtf32leToGb(const uint32_t* utf32, const void* profile, uint8_t* gbBuffer, uint32_t gbMax, uint32_t* gbSize) { - return utf32ToProfiled(isGbk, utf32, profile, gbBuffer, gbMax, gbSize); + return utf32ToProfiled(isGb, utf32, profile, gbBuffer, gbMax, gbSize); } int APS5_VABI sceCesEucKrToUcs2(const void* profile, const uint8_t* eucBuffer, uint32_t eucMax, uint32_t* eucSize, uint16_t* ucs2) { diff --git a/core/libs/prx/libSceCesCs/Gb18030.hpp b/core/libs/prx/libSceCesCs/Gb18030.hpp new file mode 100644 index 0000000000..5f13d60813 --- /dev/null +++ b/core/libs/prx/libSceCesCs/Gb18030.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include + +namespace Gb18030 { + +inline constexpr uint32_t TWO_BYTE_2000[111][3] = { + {0x17C0, 0xE4C6, 0x60}, {0x187E, 0xE526, 0x60}, {0x18E8, 0xE766, 0x6}, {0x1920, 0x20AC, 0x1}, {0x1921, 0xE76D, 0x1}, {0x192C, 0xE76E, 0x2}, + {0x193A, 0xE770, 0x2}, {0x193C, 0xE586, 0x60}, {0x19FA, 0xE5E6, 0x60}, {0x1AAD, 0xE772, 0xB}, {0x1AB8, 0xE646, 0x60}, {0x1B6E, 0xE77D, 0x8}, + {0x1B76, 0xE6A6, 0x60}, {0x1BEE, 0xE785, 0x8}, {0x1C0E, 0xE78D, 0x7}, {0x1C21, 0xE794, 0x2}, {0x1C28, 0xE796, 0x1}, {0x1C2B, 0xE797, 0x9}, + {0x1C34, 0xE706, 0x60}, {0x1CB5, 0xE7A0, 0xF}, {0x1CE5, 0xE7AF, 0xD}, {0x1D47, 0xE7BC, 0xB}, {0x1D6D, 0xE7C7, 0x1}, {0x1D70, 0x1F9, 0x1}, + {0x1D72, 0xE7C9, 0x4}, {0x1D9B, 0xE7CD, 0x15}, {0x1DC8, 0xE7E2, 0x1}, {0x1DCB, 0xE7E3, 0x1}, {0x1DCD, 0xE7E4, 0x3}, {0x1DF8, 0x303E, 0x1}, + {0x1DF9, 0x2FF0, 0xC}, {0x1E06, 0xE7F4, 0xD}, {0x1E5F, 0xE801, 0xF}, {0x1ECE, 0xE000, 0x5E}, {0x1F8C, 0xE05E, 0x5E}, {0x204A, 0xE0BC, 0x5E}, + {0x2108, 0xE11A, 0x5E}, {0x21C6, 0xE178, 0x5E}, {0x2284, 0xE1D6, 0x5E}, {0x408D, 0xE810, 0x5}, {0x58B2, 0xE234, 0x5E}, {0x5970, 0xE292, 0x5E}, + {0x5A2E, 0xE2F0, 0x5E}, {0x5AEC, 0xE34E, 0x5E}, {0x5BAA, 0xE3AC, 0x5E}, {0x5C68, 0xE40A, 0x5E}, {0x5CD6, 0x2E81, 0x1}, {0x5CD7, 0xE816, 0x3}, + {0x5CDA, 0x2E84, 0x1}, {0x5CDB, 0x3473, 0x1}, {0x5CDC, 0x3447, 0x1}, {0x5CDD, 0x2E88, 0x1}, {0x5CDE, 0x2E8B, 0x1}, {0x5CDF, 0xE81E, 0x1}, + {0x5CE0, 0x359E, 0x1}, {0x5CE1, 0x361A, 0x1}, {0x5CE2, 0x360E, 0x1}, {0x5CE3, 0x2E8C, 0x1}, {0x5CE4, 0x2E97, 0x1}, {0x5CE5, 0x396E, 0x1}, + {0x5CE6, 0x3918, 0x1}, {0x5CE7, 0xE826, 0x1}, {0x5CE8, 0x39CF, 0x1}, {0x5CE9, 0x39DF, 0x1}, {0x5CEA, 0x3A73, 0x1}, {0x5CEB, 0x39D0, 0x1}, + {0x5CEC, 0xE82B, 0x2}, {0x5CEE, 0x3B4E, 0x1}, {0x5CEF, 0x3C6E, 0x1}, {0x5CF0, 0x3CE0, 0x1}, {0x5CF1, 0x2EA7, 0x1}, {0x5CF2, 0xE831, 0x2}, + {0x5CF4, 0x2EAA, 0x1}, {0x5CF5, 0x4056, 0x1}, {0x5CF6, 0x415F, 0x1}, {0x5CF7, 0x2EAE, 0x1}, {0x5CF8, 0x4337, 0x1}, {0x5CF9, 0x2EB3, 0x1}, + {0x5CFA, 0x2EB6, 0x2}, {0x5CFC, 0xE83B, 0x1}, {0x5CFD, 0x43B1, 0x1}, {0x5CFE, 0x43AC, 0x1}, {0x5CFF, 0x2EBB, 0x1}, {0x5D00, 0x43DD, 0x1}, + {0x5D01, 0x44D6, 0x1}, {0x5D02, 0x4661, 0x1}, {0x5D03, 0x464C, 0x1}, {0x5D04, 0xE843, 0x1}, {0x5D05, 0x4723, 0x1}, {0x5D06, 0x4729, 0x1}, + {0x5D07, 0x477C, 0x1}, {0x5D08, 0x478D, 0x1}, {0x5D09, 0x2ECA, 0x1}, {0x5D0A, 0x4947, 0x1}, {0x5D0B, 0x497A, 0x1}, {0x5D0C, 0x497D, 0x1}, + {0x5D0D, 0x4982, 0x2}, {0x5D0F, 0x4985, 0x2}, {0x5D11, 0x499F, 0x1}, {0x5D12, 0x499B, 0x1}, {0x5D13, 0x49B7, 0x1}, {0x5D14, 0x49B6, 0x1}, + {0x5D15, 0xE854, 0x2}, {0x5D17, 0x4CA3, 0x1}, {0x5D18, 0x4C9F, 0x3}, {0x5D1B, 0x4C77, 0x1}, {0x5D1C, 0x4CA2, 0x1}, {0x5D1D, 0x4D13, 0x7}, + {0x5D24, 0x4DAE, 0x1}, {0x5D25, 0xE864, 0x1}, {0x5D26, 0xE468, 0x5E}, +}; + +inline constexpr uint32_t FOUR_BYTE_2000[207][3] = { + {0x0, 0x80, 0x24}, {0x24, 0xA5, 0x2}, {0x26, 0xA9, 0x7}, {0x2D, 0xB2, 0x5}, {0x32, 0xB8, 0x1F}, {0x51, 0xD8, 0x8}, + {0x59, 0xE2, 0x6}, {0x5F, 0xEB, 0x1}, {0x60, 0xEE, 0x4}, {0x64, 0xF4, 0x3}, {0x67, 0xF8, 0x1}, {0x68, 0xFB, 0x1}, + {0x69, 0xFD, 0x4}, {0x6D, 0x102, 0x11}, {0x7E, 0x114, 0x7}, {0x85, 0x11C, 0xF}, {0x94, 0x12C, 0x18}, {0xAC, 0x145, 0x3}, + {0xAF, 0x149, 0x4}, {0xB3, 0x14E, 0x1D}, {0xD0, 0x16C, 0x62}, {0x132, 0x1CF, 0x1}, {0x133, 0x1D1, 0x1}, {0x134, 0x1D3, 0x1}, + {0x135, 0x1D5, 0x1}, {0x136, 0x1D7, 0x1}, {0x137, 0x1D9, 0x1}, {0x138, 0x1DB, 0x1}, {0x139, 0x1DD, 0x1C}, {0x155, 0x1FA, 0x57}, + {0x1AC, 0x252, 0xF}, {0x1BB, 0x262, 0x65}, {0x220, 0x2C8, 0x1}, {0x221, 0x2CC, 0xD}, {0x22E, 0x2DA, 0xB7}, {0x2E5, 0x3A2, 0x1}, + {0x2E6, 0x3AA, 0x7}, {0x2ED, 0x3C2, 0x1}, {0x2EE, 0x3CA, 0x37}, {0x325, 0x402, 0xE}, {0x333, 0x450, 0x1}, {0x334, 0x452, 0x1BBE}, + {0x1EF2, 0x2011, 0x2}, {0x1EF4, 0x2017, 0x1}, {0x1EF5, 0x201A, 0x2}, {0x1EF7, 0x201E, 0x7}, {0x1EFE, 0x2027, 0x9}, {0x1F07, 0x2031, 0x1}, + {0x1F08, 0x2034, 0x1}, {0x1F09, 0x2036, 0x5}, {0x1F0E, 0x203C, 0x70}, {0x1F7E, 0x20AD, 0x56}, {0x1FD4, 0x2104, 0x1}, {0x1FD5, 0x2106, 0x3}, + {0x1FD8, 0x210A, 0xC}, {0x1FE4, 0x2117, 0xA}, {0x1FEE, 0x2122, 0x3E}, {0x202C, 0x216C, 0x4}, {0x2030, 0x217A, 0x16}, {0x2046, 0x2194, 0x2}, + {0x2048, 0x219A, 0x6E}, {0x20B6, 0x2209, 0x6}, {0x20BC, 0x2210, 0x1}, {0x20BD, 0x2212, 0x3}, {0x20C0, 0x2216, 0x4}, {0x20C4, 0x221B, 0x2}, + {0x20C6, 0x2221, 0x2}, {0x20C8, 0x2224, 0x1}, {0x20C9, 0x2226, 0x1}, {0x20CA, 0x222C, 0x2}, {0x20CC, 0x222F, 0x5}, {0x20D1, 0x2238, 0x5}, + {0x20D6, 0x223E, 0xA}, {0x20E0, 0x2249, 0x3}, {0x20E3, 0x224D, 0x5}, {0x20E8, 0x2253, 0xD}, {0x20F5, 0x2262, 0x2}, {0x20F7, 0x2268, 0x6}, + {0x20FD, 0x2270, 0x25}, {0x2122, 0x2296, 0x3}, {0x2125, 0x229A, 0xB}, {0x2130, 0x22A6, 0x19}, {0x2149, 0x22C0, 0x52}, {0x219B, 0x2313, 0x14D}, + {0x22E8, 0x246A, 0xA}, {0x22F2, 0x249C, 0x64}, {0x2356, 0x254C, 0x4}, {0x235A, 0x2574, 0xD}, {0x2367, 0x2590, 0x3}, {0x236A, 0x2596, 0xA}, + {0x2374, 0x25A2, 0x10}, {0x2384, 0x25B4, 0x8}, {0x238C, 0x25BE, 0x8}, {0x2394, 0x25C8, 0x3}, {0x2397, 0x25CC, 0x2}, {0x2399, 0x25D0, 0x12}, + {0x23AB, 0x25E6, 0x1F}, {0x23CA, 0x2607, 0x2}, {0x23CC, 0x260A, 0x36}, {0x2402, 0x2641, 0x1}, {0x2403, 0x2643, 0x83E}, {0x2C41, 0x2E82, 0x2}, + {0x2C43, 0x2E85, 0x3}, {0x2C46, 0x2E89, 0x2}, {0x2C48, 0x2E8D, 0xA}, {0x2C52, 0x2E98, 0xF}, {0x2C61, 0x2EA8, 0x2}, {0x2C63, 0x2EAB, 0x3}, + {0x2C66, 0x2EAF, 0x4}, {0x2C6A, 0x2EB4, 0x2}, {0x2C6C, 0x2EB8, 0x3}, {0x2C6F, 0x2EBC, 0xE}, {0x2C7D, 0x2ECB, 0x125}, {0x2DA2, 0x2FFC, 0x4}, + {0x2DA6, 0x3004, 0x1}, {0x2DA7, 0x3018, 0x5}, {0x2DAC, 0x301F, 0x2}, {0x2DAE, 0x302A, 0x14}, {0x2DC2, 0x303F, 0x2}, {0x2DC4, 0x3094, 0x7}, + {0x2DCB, 0x309F, 0x2}, {0x2DCD, 0x30F7, 0x5}, {0x2DD2, 0x30FF, 0x6}, {0x2DD8, 0x312A, 0xF6}, {0x2ECE, 0x322A, 0x7}, {0x2ED5, 0x3232, 0x71}, + {0x2F46, 0x32A4, 0xEA}, {0x3030, 0x3390, 0xC}, {0x303C, 0x339F, 0x2}, {0x303E, 0x33A2, 0x22}, {0x3060, 0x33C5, 0x9}, {0x3069, 0x33CF, 0x2}, + {0x306B, 0x33D3, 0x2}, {0x306D, 0x33D6, 0x71}, {0x30DE, 0x3448, 0x2B}, {0x3109, 0x3474, 0x12A}, {0x3233, 0x359F, 0x6F}, {0x32A2, 0x360F, 0xB}, + {0x32AD, 0x361B, 0x2FD}, {0x35AA, 0x3919, 0x55}, {0x35FF, 0x396F, 0x60}, {0x365F, 0x39D1, 0xE}, {0x366D, 0x39E0, 0x93}, {0x3700, 0x3A74, 0xDA}, + {0x37DA, 0x3B4F, 0x11F}, {0x38F9, 0x3C6F, 0x71}, {0x396A, 0x3CE1, 0x375}, {0x3CDF, 0x4057, 0x108}, {0x3DE7, 0x4160, 0x1D7}, {0x3FBE, 0x4338, 0x74}, + {0x4032, 0x43AD, 0x4}, {0x4036, 0x43B2, 0x2B}, {0x4061, 0x43DE, 0xF8}, {0x4159, 0x44D7, 0x175}, {0x42CE, 0x464D, 0x14}, {0x42E2, 0x4662, 0xC1}, + {0x43A3, 0x4724, 0x5}, {0x43A8, 0x472A, 0x52}, {0x43FA, 0x477D, 0x10}, {0x440A, 0x478E, 0x1B9}, {0x45C3, 0x4948, 0x32}, {0x45F5, 0x497B, 0x2}, + {0x45F7, 0x497E, 0x4}, {0x45FB, 0x4984, 0x1}, {0x45FC, 0x4987, 0x14}, {0x4610, 0x499C, 0x3}, {0x4613, 0x49A0, 0x16}, {0x4629, 0x49B8, 0x2BF}, + {0x48E8, 0x4C78, 0x27}, {0x490F, 0x4CA4, 0x6F}, {0x497E, 0x4D1A, 0x94}, {0x4A12, 0x4DAF, 0x51}, {0x4A63, 0x9FA6, 0x385A}, {0x82BD, 0xE76C, 0x1}, + {0x82BE, 0xE7C8, 0x1}, {0x82BF, 0xE7E7, 0xD}, {0x82CC, 0xE815, 0x1}, {0x82CD, 0xE819, 0x5}, {0x82D2, 0xE81F, 0x7}, {0x82D9, 0xE827, 0x4}, + {0x82DD, 0xE82D, 0x4}, {0x82E1, 0xE833, 0x8}, {0x82E9, 0xE83C, 0x7}, {0x82F0, 0xE844, 0x10}, {0x8300, 0xE856, 0xE}, {0x830E, 0xE865, 0x10C7}, + {0x93D5, 0xF92D, 0x4C}, {0x9421, 0xF97A, 0x1B}, {0x943C, 0xF996, 0x51}, {0x948D, 0xF9E8, 0x9}, {0x9496, 0xF9F2, 0x1A}, {0x94B0, 0xFA10, 0x1}, + {0x94B1, 0xFA12, 0x1}, {0x94B2, 0xFA15, 0x3}, {0x94B5, 0xFA19, 0x6}, {0x94BB, 0xFA22, 0x1}, {0x94BC, 0xFA25, 0x2}, {0x94BE, 0xFA2A, 0x406}, + {0x98C4, 0xFE32, 0x1}, {0x98C5, 0xFE45, 0x4}, {0x98C9, 0xFE53, 0x1}, {0x98CA, 0xFE58, 0x1}, {0x98CB, 0xFE67, 0x1}, {0x98CC, 0xFE6C, 0x95}, + {0x9961, 0xFF5F, 0x81}, {0x99E2, 0xFFE6, 0x1A}, {0x2E248, 0x10000, 0x100000}, +}; + +inline constexpr uint32_t TWO_BYTE_2005[118][3] = { + {0x17C0, 0xE4C6, 0x60}, {0x187E, 0xE526, 0x60}, {0x18E8, 0xE766, 0x6}, {0x1920, 0x20AC, 0x1}, {0x1921, 0xE76D, 0x1}, {0x192C, 0xE76E, 0x2}, + {0x193A, 0xE770, 0x2}, {0x193C, 0xE586, 0x60}, {0x19FA, 0xE5E6, 0x60}, {0x1AAD, 0xE772, 0xB}, {0x1AB8, 0xE646, 0x60}, {0x1B6E, 0xE77D, 0x8}, + {0x1B76, 0xE6A6, 0x60}, {0x1BEE, 0xE785, 0x8}, {0x1C0E, 0xFE10, 0x1}, {0x1C0F, 0xFE12, 0x1}, {0x1C10, 0xFE11, 0x1}, {0x1C11, 0xFE13, 0x4}, + {0x1C21, 0xFE17, 0x2}, {0x1C28, 0xFE19, 0x1}, {0x1C2B, 0xE797, 0x9}, {0x1C34, 0xE706, 0x60}, {0x1CB5, 0xE7A0, 0xF}, {0x1CE5, 0xE7AF, 0xD}, + {0x1D47, 0xE7BC, 0xB}, {0x1D6D, 0x1E3F, 0x1}, {0x1D70, 0x1F9, 0x1}, {0x1D72, 0xE7C9, 0x4}, {0x1D9B, 0xE7CD, 0x15}, {0x1DC8, 0xE7E2, 0x1}, + {0x1DCB, 0xE7E3, 0x1}, {0x1DCD, 0xE7E4, 0x3}, {0x1DF8, 0x303E, 0x1}, {0x1DF9, 0x2FF0, 0xC}, {0x1E06, 0xE7F4, 0xD}, {0x1E5F, 0xE801, 0xF}, + {0x1ECE, 0xE000, 0x5E}, {0x1F8C, 0xE05E, 0x5E}, {0x204A, 0xE0BC, 0x5E}, {0x2108, 0xE11A, 0x5E}, {0x21C6, 0xE178, 0x5E}, {0x2284, 0xE1D6, 0x5E}, + {0x408D, 0xE810, 0x5}, {0x58B2, 0xE234, 0x5E}, {0x5970, 0xE292, 0x5E}, {0x5A2E, 0xE2F0, 0x5E}, {0x5AEC, 0xE34E, 0x5E}, {0x5BAA, 0xE3AC, 0x5E}, + {0x5C68, 0xE40A, 0x5E}, {0x5CD6, 0x2E81, 0x1}, {0x5CD7, 0x20087, 0x1}, {0x5CD8, 0x20089, 0x1}, {0x5CD9, 0x200CC, 0x1}, {0x5CDA, 0x2E84, 0x1}, + {0x5CDB, 0x3473, 0x1}, {0x5CDC, 0x3447, 0x1}, {0x5CDD, 0x2E88, 0x1}, {0x5CDE, 0x2E8B, 0x1}, {0x5CDF, 0x9FB4, 0x1}, {0x5CE0, 0x359E, 0x1}, + {0x5CE1, 0x361A, 0x1}, {0x5CE2, 0x360E, 0x1}, {0x5CE3, 0x2E8C, 0x1}, {0x5CE4, 0x2E97, 0x1}, {0x5CE5, 0x396E, 0x1}, {0x5CE6, 0x3918, 0x1}, + {0x5CE7, 0x9FB5, 0x1}, {0x5CE8, 0x39CF, 0x1}, {0x5CE9, 0x39DF, 0x1}, {0x5CEA, 0x3A73, 0x1}, {0x5CEB, 0x39D0, 0x1}, {0x5CEC, 0x9FB6, 0x2}, + {0x5CEE, 0x3B4E, 0x1}, {0x5CEF, 0x3C6E, 0x1}, {0x5CF0, 0x3CE0, 0x1}, {0x5CF1, 0x2EA7, 0x1}, {0x5CF2, 0x215D7, 0x1}, {0x5CF3, 0x9FB8, 0x1}, + {0x5CF4, 0x2EAA, 0x1}, {0x5CF5, 0x4056, 0x1}, {0x5CF6, 0x415F, 0x1}, {0x5CF7, 0x2EAE, 0x1}, {0x5CF8, 0x4337, 0x1}, {0x5CF9, 0x2EB3, 0x1}, + {0x5CFA, 0x2EB6, 0x2}, {0x5CFC, 0x2298F, 0x1}, {0x5CFD, 0x43B1, 0x1}, {0x5CFE, 0x43AC, 0x1}, {0x5CFF, 0x2EBB, 0x1}, {0x5D00, 0x43DD, 0x1}, + {0x5D01, 0x44D6, 0x1}, {0x5D02, 0x4661, 0x1}, {0x5D03, 0x464C, 0x1}, {0x5D04, 0x9FB9, 0x1}, {0x5D05, 0x4723, 0x1}, {0x5D06, 0x4729, 0x1}, + {0x5D07, 0x477C, 0x1}, {0x5D08, 0x478D, 0x1}, {0x5D09, 0x2ECA, 0x1}, {0x5D0A, 0x4947, 0x1}, {0x5D0B, 0x497A, 0x1}, {0x5D0C, 0x497D, 0x1}, + {0x5D0D, 0x4982, 0x2}, {0x5D0F, 0x4985, 0x2}, {0x5D11, 0x499F, 0x1}, {0x5D12, 0x499B, 0x1}, {0x5D13, 0x49B7, 0x1}, {0x5D14, 0x49B6, 0x1}, + {0x5D15, 0x9FBA, 0x1}, {0x5D16, 0x241FE, 0x1}, {0x5D17, 0x4CA3, 0x1}, {0x5D18, 0x4C9F, 0x3}, {0x5D1B, 0x4C77, 0x1}, {0x5D1C, 0x4CA2, 0x1}, + {0x5D1D, 0x4D13, 0x7}, {0x5D24, 0x4DAE, 0x1}, {0x5D25, 0x9FBB, 0x1}, {0x5D26, 0xE468, 0x5E}, +}; + +inline constexpr uint32_t FOUR_BYTE_2005[209][3] = { + {0x0, 0x80, 0x24}, {0x24, 0xA5, 0x2}, {0x26, 0xA9, 0x7}, {0x2D, 0xB2, 0x5}, {0x32, 0xB8, 0x1F}, {0x51, 0xD8, 0x8}, + {0x59, 0xE2, 0x6}, {0x5F, 0xEB, 0x1}, {0x60, 0xEE, 0x4}, {0x64, 0xF4, 0x3}, {0x67, 0xF8, 0x1}, {0x68, 0xFB, 0x1}, + {0x69, 0xFD, 0x4}, {0x6D, 0x102, 0x11}, {0x7E, 0x114, 0x7}, {0x85, 0x11C, 0xF}, {0x94, 0x12C, 0x18}, {0xAC, 0x145, 0x3}, + {0xAF, 0x149, 0x4}, {0xB3, 0x14E, 0x1D}, {0xD0, 0x16C, 0x62}, {0x132, 0x1CF, 0x1}, {0x133, 0x1D1, 0x1}, {0x134, 0x1D3, 0x1}, + {0x135, 0x1D5, 0x1}, {0x136, 0x1D7, 0x1}, {0x137, 0x1D9, 0x1}, {0x138, 0x1DB, 0x1}, {0x139, 0x1DD, 0x1C}, {0x155, 0x1FA, 0x57}, + {0x1AC, 0x252, 0xF}, {0x1BB, 0x262, 0x65}, {0x220, 0x2C8, 0x1}, {0x221, 0x2CC, 0xD}, {0x22E, 0x2DA, 0xB7}, {0x2E5, 0x3A2, 0x1}, + {0x2E6, 0x3AA, 0x7}, {0x2ED, 0x3C2, 0x1}, {0x2EE, 0x3CA, 0x37}, {0x325, 0x402, 0xE}, {0x333, 0x450, 0x1}, {0x334, 0x452, 0x19ED}, + {0x1D21, 0xE7C7, 0x1}, {0x1D22, 0x1E40, 0x1D0}, {0x1EF2, 0x2011, 0x2}, {0x1EF4, 0x2017, 0x1}, {0x1EF5, 0x201A, 0x2}, {0x1EF7, 0x201E, 0x7}, + {0x1EFE, 0x2027, 0x9}, {0x1F07, 0x2031, 0x1}, {0x1F08, 0x2034, 0x1}, {0x1F09, 0x2036, 0x5}, {0x1F0E, 0x203C, 0x70}, {0x1F7E, 0x20AD, 0x56}, + {0x1FD4, 0x2104, 0x1}, {0x1FD5, 0x2106, 0x3}, {0x1FD8, 0x210A, 0xC}, {0x1FE4, 0x2117, 0xA}, {0x1FEE, 0x2122, 0x3E}, {0x202C, 0x216C, 0x4}, + {0x2030, 0x217A, 0x16}, {0x2046, 0x2194, 0x2}, {0x2048, 0x219A, 0x6E}, {0x20B6, 0x2209, 0x6}, {0x20BC, 0x2210, 0x1}, {0x20BD, 0x2212, 0x3}, + {0x20C0, 0x2216, 0x4}, {0x20C4, 0x221B, 0x2}, {0x20C6, 0x2221, 0x2}, {0x20C8, 0x2224, 0x1}, {0x20C9, 0x2226, 0x1}, {0x20CA, 0x222C, 0x2}, + {0x20CC, 0x222F, 0x5}, {0x20D1, 0x2238, 0x5}, {0x20D6, 0x223E, 0xA}, {0x20E0, 0x2249, 0x3}, {0x20E3, 0x224D, 0x5}, {0x20E8, 0x2253, 0xD}, + {0x20F5, 0x2262, 0x2}, {0x20F7, 0x2268, 0x6}, {0x20FD, 0x2270, 0x25}, {0x2122, 0x2296, 0x3}, {0x2125, 0x229A, 0xB}, {0x2130, 0x22A6, 0x19}, + {0x2149, 0x22C0, 0x52}, {0x219B, 0x2313, 0x14D}, {0x22E8, 0x246A, 0xA}, {0x22F2, 0x249C, 0x64}, {0x2356, 0x254C, 0x4}, {0x235A, 0x2574, 0xD}, + {0x2367, 0x2590, 0x3}, {0x236A, 0x2596, 0xA}, {0x2374, 0x25A2, 0x10}, {0x2384, 0x25B4, 0x8}, {0x238C, 0x25BE, 0x8}, {0x2394, 0x25C8, 0x3}, + {0x2397, 0x25CC, 0x2}, {0x2399, 0x25D0, 0x12}, {0x23AB, 0x25E6, 0x1F}, {0x23CA, 0x2607, 0x2}, {0x23CC, 0x260A, 0x36}, {0x2402, 0x2641, 0x1}, + {0x2403, 0x2643, 0x83E}, {0x2C41, 0x2E82, 0x2}, {0x2C43, 0x2E85, 0x3}, {0x2C46, 0x2E89, 0x2}, {0x2C48, 0x2E8D, 0xA}, {0x2C52, 0x2E98, 0xF}, + {0x2C61, 0x2EA8, 0x2}, {0x2C63, 0x2EAB, 0x3}, {0x2C66, 0x2EAF, 0x4}, {0x2C6A, 0x2EB4, 0x2}, {0x2C6C, 0x2EB8, 0x3}, {0x2C6F, 0x2EBC, 0xE}, + {0x2C7D, 0x2ECB, 0x125}, {0x2DA2, 0x2FFC, 0x4}, {0x2DA6, 0x3004, 0x1}, {0x2DA7, 0x3018, 0x5}, {0x2DAC, 0x301F, 0x2}, {0x2DAE, 0x302A, 0x14}, + {0x2DC2, 0x303F, 0x2}, {0x2DC4, 0x3094, 0x7}, {0x2DCB, 0x309F, 0x2}, {0x2DCD, 0x30F7, 0x5}, {0x2DD2, 0x30FF, 0x6}, {0x2DD8, 0x312A, 0xF6}, + {0x2ECE, 0x322A, 0x7}, {0x2ED5, 0x3232, 0x71}, {0x2F46, 0x32A4, 0xEA}, {0x3030, 0x3390, 0xC}, {0x303C, 0x339F, 0x2}, {0x303E, 0x33A2, 0x22}, + {0x3060, 0x33C5, 0x9}, {0x3069, 0x33CF, 0x2}, {0x306B, 0x33D3, 0x2}, {0x306D, 0x33D6, 0x71}, {0x30DE, 0x3448, 0x2B}, {0x3109, 0x3474, 0x12A}, + {0x3233, 0x359F, 0x6F}, {0x32A2, 0x360F, 0xB}, {0x32AD, 0x361B, 0x2FD}, {0x35AA, 0x3919, 0x55}, {0x35FF, 0x396F, 0x60}, {0x365F, 0x39D1, 0xE}, + {0x366D, 0x39E0, 0x93}, {0x3700, 0x3A74, 0xDA}, {0x37DA, 0x3B4F, 0x11F}, {0x38F9, 0x3C6F, 0x71}, {0x396A, 0x3CE1, 0x375}, {0x3CDF, 0x4057, 0x108}, + {0x3DE7, 0x4160, 0x1D7}, {0x3FBE, 0x4338, 0x74}, {0x4032, 0x43AD, 0x4}, {0x4036, 0x43B2, 0x2B}, {0x4061, 0x43DE, 0xF8}, {0x4159, 0x44D7, 0x175}, + {0x42CE, 0x464D, 0x14}, {0x42E2, 0x4662, 0xC1}, {0x43A3, 0x4724, 0x5}, {0x43A8, 0x472A, 0x52}, {0x43FA, 0x477D, 0x10}, {0x440A, 0x478E, 0x1B9}, + {0x45C3, 0x4948, 0x32}, {0x45F5, 0x497B, 0x2}, {0x45F7, 0x497E, 0x4}, {0x45FB, 0x4984, 0x1}, {0x45FC, 0x4987, 0x14}, {0x4610, 0x499C, 0x3}, + {0x4613, 0x49A0, 0x16}, {0x4629, 0x49B8, 0x2BF}, {0x48E8, 0x4C78, 0x27}, {0x490F, 0x4CA4, 0x6F}, {0x497E, 0x4D1A, 0x94}, {0x4A12, 0x4DAF, 0x51}, + {0x4A63, 0x9FA6, 0x385A}, {0x82BD, 0xE76C, 0x1}, {0x82BE, 0xE7C8, 0x1}, {0x82BF, 0xE7E7, 0xD}, {0x82CC, 0xE815, 0x1}, {0x82CD, 0xE819, 0x5}, + {0x82D2, 0xE81F, 0x7}, {0x82D9, 0xE827, 0x4}, {0x82DD, 0xE82D, 0x4}, {0x82E1, 0xE833, 0x8}, {0x82E9, 0xE83C, 0x7}, {0x82F0, 0xE844, 0x10}, + {0x8300, 0xE856, 0xE}, {0x830E, 0xE865, 0x10C7}, {0x93D5, 0xF92D, 0x4C}, {0x9421, 0xF97A, 0x1B}, {0x943C, 0xF996, 0x51}, {0x948D, 0xF9E8, 0x9}, + {0x9496, 0xF9F2, 0x1A}, {0x94B0, 0xFA10, 0x1}, {0x94B1, 0xFA12, 0x1}, {0x94B2, 0xFA15, 0x3}, {0x94B5, 0xFA19, 0x6}, {0x94BB, 0xFA22, 0x1}, + {0x94BC, 0xFA25, 0x2}, {0x94BE, 0xFA2A, 0x406}, {0x98C4, 0xFE32, 0x1}, {0x98C5, 0xFE45, 0x4}, {0x98C9, 0xFE53, 0x1}, {0x98CA, 0xFE58, 0x1}, + {0x98CB, 0xFE67, 0x1}, {0x98CC, 0xFE6C, 0x95}, {0x9961, 0xFF5F, 0x81}, {0x99E2, 0xFFE6, 0x1A}, {0x2E248, 0x10000, 0x100000}, +}; + +inline constexpr uint32_t FALLBACK_2005[24][2] = { + {0xE78D, 0x1C0E}, {0xE78E, 0x1C0F}, {0xE78F, 0x1C10}, {0xE790, 0x1C11}, {0xE791, 0x1C12}, {0xE792, 0x1C13}, + {0xE793, 0x1C14}, {0xE794, 0x1C21}, {0xE795, 0x1C22}, {0xE796, 0x1C28}, {0xE816, 0x5CD7}, {0xE817, 0x5CD8}, + {0xE818, 0x5CD9}, {0xE81E, 0x5CDF}, {0xE826, 0x5CE7}, {0xE82B, 0x5CEC}, {0xE82C, 0x5CED}, {0xE831, 0x5CF2}, + {0xE832, 0x5CF3}, {0xE83B, 0x5CFC}, {0xE843, 0x5D04}, {0xE854, 0x5D15}, {0xE855, 0x5D16}, {0xE864, 0x5D25}, +}; + +} diff --git a/core/libs/prx/libSceCesCs/PendingExports.cpp b/core/libs/prx/libSceCesCs/PendingExports.cpp index 3680b257df..1c38e00eed 100644 --- a/core/libs/prx/libSceCesCs/PendingExports.cpp +++ b/core/libs/prx/libSceCesCs/PendingExports.cpp @@ -257,16 +257,6 @@ int APS5_VABI sceCesUcsProfileInitEucJis2004(void) { return 0; } -int APS5_VABI sceCesUcsProfileInitGb18030_2000(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceCesUcsProfileInitGb18030_2005(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceCesUcsProfileInitIso2022JpCp50221(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/tests/GuestCesChinese.cpp b/core/libs/tests/GuestCesChinese.cpp index 8e4f16cf5a..5c3b00ea29 100644 --- a/core/libs/tests/GuestCesChinese.cpp +++ b/core/libs/tests/GuestCesChinese.cpp @@ -25,21 +25,254 @@ int APS5_VABI sceCesUtf32ToEucCn(std::uint32_t, const void*, std::uint8_t*, std: int APS5_VABI sceCesUtf32beToGb(const std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesUtf8StrToMbcsStr(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); int APS5_VABI sceCesMbcsStrToUtf8Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); +void* APS5_VABI sceCesUcsProfileInitGb18030_2000(void*); +void* APS5_VABI sceCesUcsProfileInitGb18030_2005(void*); +int APS5_VABI sceCesGbToUtf16be(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesGbToUtf32(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUtf16ToGb(const std::uint16_t*, std::uint32_t, std::uint32_t*, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesUtf32ToGb(std::uint32_t, const void*, std::uint8_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcToUcs2(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*); +int APS5_VABI sceCesMbcToUtf16le(const void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUcs2Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrToUtf16Str(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint16_t*, std::uint32_t, std::uint32_t*); +int APS5_VABI sceCesMbcsStrGetUtf16Len(void*, const std::uint8_t*, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceCesUtf16StrToMbcsStr(void*, const std::uint16_t*, std::uint32_t, std::uint32_t*, std::uint8_t*, std::uint32_t, std::uint32_t*); } static void Require(bool value) { if (!value) std::abort(); } static const std::uint8_t* Bytes(const char* text) { return reinterpret_cast(text); } -int main() { - constexpr int invalidParameter = static_cast(0x805C0001); - constexpr int invalidSrcBuffer = static_cast(0x805C0010); - constexpr int srcBufferEnd = static_cast(0x805C0011); - constexpr int invalidEncode = static_cast(0x805C0014); - constexpr int unassignedCode = static_cast(0x805C0020); - constexpr int invalidDstBuffer = static_cast(0x805C0030); - constexpr int dstBufferEnd = static_cast(0x805C0031); +static constexpr int invalidParameter = static_cast(0x805C0001); +static constexpr int invalidSrcBuffer = static_cast(0x805C0010); +static constexpr int srcBufferEnd = static_cast(0x805C0011); +static constexpr int invalidEncode = static_cast(0x805C0014); +static constexpr int unassignedCode = static_cast(0x805C0020); +static constexpr int invalidDstBuffer = static_cast(0x805C0030); +static constexpr int dstBufferEnd = static_cast(0x805C0031); +struct Gb18030Sequence { + std::uint32_t bytes; + std::uint32_t code2000; + std::uint32_t code2005; +}; + +struct Gb18030Difference { + std::uint32_t bytes; + std::uint32_t code2000; + std::uint32_t code2005; + bool fallback; +}; + +static constexpr Gb18030Sequence GB18030_BOUNDARIES[88] = { + {0x81308130, 0x80, 0x80}, {0x8130D239, 0x450, 0x450}, {0x8130D330, 0x452, 0x452}, {0x8135F436, 0x1E3E, 0x1E3E}, + {0x8135F437, 0x1E3F, 0xE7C7}, {0x8135F438, 0x1E40, 0x1E40}, {0x8136A531, 0x200F, 0x200F}, {0x8136A532, 0x2011, 0x2011}, + {0x8137A838, 0x2641, 0x2641}, {0x8137A839, 0x2643, 0x2643}, {0x8138FD38, 0x2E80, 0x2E80}, {0x8138FD39, 0x2E82, 0x2E82}, + {0x8230A632, 0x3619, 0x3619}, {0x8230A633, 0x361B, 0x361B}, {0x8230F237, 0x3917, 0x3917}, {0x8230F238, 0x3919, 0x3919}, + {0x8231D437, 0x3CDF, 0x3CDF}, {0x8231D438, 0x3CE1, 0x3CE1}, {0x8232AF32, 0x4055, 0x4055}, {0x8232AF33, 0x4057, 0x4057}, + {0x8232C936, 0x415E, 0x415E}, {0x8232C937, 0x4160, 0x4160}, {0x8232F837, 0x4336, 0x4336}, {0x8232F838, 0x4338, 0x4338}, + {0x8233A338, 0x44D5, 0x44D5}, {0x8233A339, 0x44D7, 0x44D7}, {0x8233C931, 0x464B, 0x464B}, {0x8233C932, 0x464D, 0x464D}, + {0x8233E837, 0x478C, 0x478C}, {0x8233E838, 0x478E, 0x478E}, {0x82349638, 0x4946, 0x4946}, {0x82349639, 0x4948, 0x4948}, + {0x8234A130, 0x49B5, 0x49B5}, {0x8234A131, 0x49B8, 0x49B8}, {0x8234E733, 0x4C76, 0x4C76}, {0x8234E734, 0x4C78, 0x4C78}, + {0x82358F32, 0x4DFF, 0x4DFF}, {0x82358F33, 0x9FA6, 0x9FA6}, {0x8336C738, 0xD7FF, 0xD7FF}, {0x8336C739, 0xE76C, 0xE76C}, + {0x8336CF39, 0xE863, 0xE863}, {0x8336D030, 0xE865, 0xE865}, {0x84308534, 0xF92B, 0xF92B}, {0x84308535, 0xF92D, 0xF92D}, + {0x84309C37, 0xFA26, 0xFA26}, {0x84309C38, 0xFA2A, 0xFA2A}, {0x84318537, 0xFE2F, 0xFE2F}, {0x84318538, 0xFE32, 0xFE32}, + {0x8431A233, 0xFFDF, 0xFFDF}, {0x8431A234, 0xFFE6, 0xFFE6}, {0x8431A439, 0xFFFF, 0xFFFF}, {0x8431A530, 0x0, 0x0}, + {0x8F39FE39, 0x0, 0x0}, {0x90308130, 0x10000, 0x10000}, {0x95328235, 0x1FFFF, 0x1FFFF}, {0x95328236, 0x20000, 0x20000}, + {0x9A348431, 0x2FFFF, 0x2FFFF}, {0x9A348432, 0x30000, 0x30000}, {0x9F368537, 0x3FFFF, 0x3FFFF}, {0x9F368538, 0x40000, 0x40000}, + {0xA4388733, 0x4FFFF, 0x4FFFF}, {0xA4388734, 0x50000, 0x50000}, {0xAA308839, 0x5FFFF, 0x5FFFF}, {0xAA308930, 0x60000, 0x60000}, + {0xAF328A35, 0x6FFFF, 0x6FFFF}, {0xAF328A36, 0x70000, 0x70000}, {0xB4348C31, 0x7FFFF, 0x7FFFF}, {0xB4348C32, 0x80000, 0x80000}, + {0xB9368D37, 0x8FFFF, 0x8FFFF}, {0xB9368D38, 0x90000, 0x90000}, {0xBE388F33, 0x9FFFF, 0x9FFFF}, {0xBE388F34, 0xA0000, 0xA0000}, + {0xC4309039, 0xAFFFF, 0xAFFFF}, {0xC4309130, 0xB0000, 0xB0000}, {0xC9329235, 0xBFFFF, 0xBFFFF}, {0xC9329236, 0xC0000, 0xC0000}, + {0xCE349431, 0xCFFFF, 0xCFFFF}, {0xCE349432, 0xD0000, 0xD0000}, {0xD3369537, 0xDFFFF, 0xDFFFF}, {0xD3369538, 0xE0000, 0xE0000}, + {0xD8389733, 0xEFFFF, 0xEFFFF}, {0xD8389734, 0xF0000, 0xF0000}, {0xDE309839, 0xFFFFF, 0xFFFFF}, {0xDE309930, 0x100000, 0x100000}, + {0xE3329A35, 0x10FFFF, 0x10FFFF}, {0xE3329A36, 0x0, 0x0}, {0xFD308130, 0x0, 0x0}, {0xFE39FE39, 0x0, 0x0}, +}; +static constexpr Gb18030Difference GB18030_DIFFERENCES[26] = { + {0xA6D9, 0xE78D, 0xFE10, true}, {0xA6DA, 0xE78E, 0xFE12, true}, {0xA6DB, 0xE78F, 0xFE11, true}, {0xA6DC, 0xE790, 0xFE13, true}, + {0xA6DD, 0xE791, 0xFE14, true}, {0xA6DE, 0xE792, 0xFE15, true}, {0xA6DF, 0xE793, 0xFE16, true}, {0xA6EC, 0xE794, 0xFE17, true}, + {0xA6ED, 0xE795, 0xFE18, true}, {0xA6F3, 0xE796, 0xFE19, true}, {0xA8BC, 0xE7C7, 0x1E3F, false}, {0xFE51, 0xE816, 0x20087, true}, + {0xFE52, 0xE817, 0x20089, true}, {0xFE53, 0xE818, 0x200CC, true}, {0xFE59, 0xE81E, 0x9FB4, true}, {0xFE61, 0xE826, 0x9FB5, true}, + {0xFE66, 0xE82B, 0x9FB6, true}, {0xFE67, 0xE82C, 0x9FB7, true}, {0xFE6C, 0xE831, 0x215D7, true}, {0xFE6D, 0xE832, 0x9FB8, true}, + {0xFE76, 0xE83B, 0x2298F, true}, {0xFE7E, 0xE843, 0x9FB9, true}, {0xFE90, 0xE854, 0x9FBA, true}, {0xFE91, 0xE855, 0x241FE, true}, + {0xFEA0, 0xE864, 0x9FBB, true}, {0x8135F437, 0x1E3F, 0xE7C7, false}, +}; +static std::uint32_t Gb18030Bytes(std::uint32_t linear) { + return (0x81 + linear / 12600) << 24 | (0x30 + linear / 1260 % 10) << 16 | (0x81 + linear / 10 % 126) << 8 | (0x30 + linear % 10); +} + +static void Fnv(std::uint32_t& hash, std::uint32_t value) { + for (int i = 0; i < 4; ++i) { + hash ^= (value >> (8 * i)) & 0xFF; + hash *= 0x01000193; + } +} + +static std::uint32_t Gb18030Decode(const void* gb, std::uint32_t packed) { + std::uint8_t bytes[4] = {}; + const std::uint32_t size = packed > 0xFFFF ? 4 : 2; + for (std::uint32_t i = 0; i < size; ++i) bytes[i] = static_cast(packed >> (8 * (size - 1 - i))); + std::uint32_t consumed = 0x55; + std::uint32_t code = 0xDEADBEEF; + const int result = sceCesGbToUtf32(gb, bytes, size, &consumed, &code); + if (result != 0) { + Require(result == unassignedCode && consumed == 0x55 && code == 0xDEADBEEF); + return 0; + } + Require(consumed == size); + return code; +} + +static std::uint32_t Gb18030Encode(const void* gb, std::uint32_t code) { + std::uint8_t bytes[4] = {}; + std::uint32_t size = 0; + Require(sceCesUtf32ToGb(code, gb, bytes, sizeof(bytes), &size) == 0 && (size == 2 || size == 4)); + std::uint32_t packed = 0; + for (std::uint32_t i = 0; i < size; ++i) packed = packed << 8 | bytes[i]; + return packed; +} + +static void RequireGb18030Error(const void* gb, const char* bytes, std::uint32_t size, int error) { + std::uint32_t consumed = 0x55; + std::uint32_t utf32 = 0xDEADBEEF; + Require(sceCesGbToUtf32(gb, Bytes(bytes), size, &consumed, &utf32) == error && consumed == 0x55 && utf32 == 0xDEADBEEF); + std::uint16_t ucs2 = 0xBEEF; + Require(sceCesMbcToUcs2(gb, Bytes(bytes), size, &consumed, &ucs2) == error && consumed == 0x55 && ucs2 == 0xBEEF); +} + +static void CheckGb18030(const void* gb, bool revision2005, std::uint32_t twoByteHash, std::uint32_t fourByteHash, std::uint32_t encodeHash, int fourByteOneWay, int codeOneWay) { + std::uint32_t hash = 0x811C9DC5; + for (std::uint32_t cell = 0; cell < 126 * 190; ++cell) { + const std::uint32_t column = cell % 190; + const std::uint32_t bytes = (0x81 + cell / 190) << 8 | (column < 0x3F ? 0x40 + column : 0x41 + column); + const std::uint32_t code = Gb18030Decode(gb, bytes); + Require(code != 0 && Gb18030Encode(gb, code) == bytes); + Fnv(hash, code); + } + Require(hash == twoByteHash); + + hash = 0x811C9DC5; + int oneWay = 0; + for (std::uint32_t linear = 0; linear < 39420; ++linear) { + const std::uint32_t bytes = Gb18030Bytes(linear); + const std::uint32_t code = Gb18030Decode(gb, bytes); + Require(code >= 0x80 && code <= 0xFFFF); + const std::uint32_t back = Gb18030Encode(gb, code); + if (back != bytes) { + Require(back <= 0xFFFF && Gb18030Decode(gb, back) == code); + ++oneWay; + } + Fnv(hash, code); + } + Require(hash == fourByteHash && oneWay == fourByteOneWay); + + hash = 0x811C9DC5; + oneWay = 0; + for (std::uint32_t code = 0x80; code < 0x10000; ++code) { + if (code >= 0xD800 && code <= 0xDFFF) continue; + const std::uint32_t bytes = Gb18030Encode(gb, code); + if (Gb18030Decode(gb, bytes) != code) ++oneWay; + Fnv(hash, bytes); + } + Require(hash == encodeHash && oneWay == codeOneWay); + + hash = 0x811C9DC5; + int samples = 0; + for (std::uint32_t linear = 189000; linear <= 189000 + 0xFFFFF; linear += 997) { + Fnv(hash, Gb18030Decode(gb, Gb18030Bytes(linear))); + ++samples; + } + Require(hash == 0x9E77B552 && samples == 1052); + hash = 0x811C9DC5; + for (std::uint32_t code = 0x10000; code < 0x110000; code += 997) Fnv(hash, Gb18030Encode(gb, code)); + Require(hash == 0x3D088D7E); + + for (const Gb18030Sequence& entry : GB18030_BOUNDARIES) { + const std::uint32_t code = revision2005 ? entry.code2005 : entry.code2000; + Require(Gb18030Decode(gb, entry.bytes) == code); + if (code != 0) Require(Gb18030Encode(gb, code) == entry.bytes); + } + for (const Gb18030Difference& entry : GB18030_DIFFERENCES) { + const std::uint32_t code = revision2005 ? entry.code2005 : entry.code2000; + Require(Gb18030Decode(gb, entry.bytes) == code && Gb18030Encode(gb, code) == entry.bytes); + if (revision2005 && entry.fallback) Require(Gb18030Encode(gb, entry.code2000) == entry.bytes); + } + + RequireGb18030Error(gb, "\x80", 1, invalidEncode); + RequireGb18030Error(gb, "\xFF", 1, invalidEncode); + RequireGb18030Error(gb, "\x81", 1, srcBufferEnd); + RequireGb18030Error(gb, "\x81\x30", 2, srcBufferEnd); + RequireGb18030Error(gb, "\x81\x30\x81", 3, srcBufferEnd); + RequireGb18030Error(gb, "\x81\x00", 2, invalidEncode); + RequireGb18030Error(gb, "\x81\x2F", 2, invalidEncode); + RequireGb18030Error(gb, "\x81\x3A", 2, invalidEncode); + RequireGb18030Error(gb, "\x81\x7F", 2, invalidEncode); + RequireGb18030Error(gb, "\x81\xFF", 2, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\x80\x30", 4, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\xFF\x30", 4, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\x30\x30", 4, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\x81\x2F", 4, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\x81\x3A", 4, invalidEncode); + RequireGb18030Error(gb, "\x81\x30\x81\x81", 4, invalidEncode); + RequireGb18030Error(gb, "\x84\x31\xA5\x30", 4, unassignedCode); + RequireGb18030Error(gb, "\xE3\x32\x9A\x36", 4, unassignedCode); + RequireGb18030Error(gb, "\xFE\x39\xFE\x39", 4, unassignedCode); + + std::uint32_t consumed = 0x55; + std::uint32_t produced = 0x55; + std::uint16_t ucs2 = 0xBEEF; + Require(sceCesGbToUcs2(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, &ucs2) == unassignedCode && consumed == 0x55 && ucs2 == 0xBEEF); + std::uint8_t utf8[4] = {1, 2, 3, 4}; + Require(sceCesGbToUtf8(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf8, 3, &produced) == dstBufferEnd && consumed == 0x55 && produced == 0x55); + Require(std::memcmp(utf8, "\x01\x02\x03\x04", 4) == 0); + Require(sceCesGbToUtf8(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf8, 4, &produced) == 0 && consumed == 4 && produced == 4); + Require(std::memcmp(utf8, "\xF0\x90\x80\x80", 4) == 0); + std::uint16_t utf16[2] = {0xBEEF, 0xBEEF}; + produced = 0x55; + Require(sceCesGbToUtf16(gb, Bytes("\xE3\x32\x9A\x35"), 4, &consumed, utf16, 1, &produced) == dstBufferEnd && produced == 0x55 && utf16[0] == 0xBEEF); + Require(sceCesGbToUtf16(gb, Bytes("\xE3\x32\x9A\x35"), 4, &consumed, utf16, 2, &produced) == 0 && produced == 2 && utf16[0] == 0xDBFF && utf16[1] == 0xDFFF); + Require(sceCesGbToUtf16be(gb, Bytes("\x90\x30\x81\x30"), 4, &consumed, utf16, 2, &produced) == 0 && std::memcmp(utf16, "\xD8\x00\xDC\x00", 4) == 0); + Require(sceCesMbcToUtf16le(gb, Bytes("\x94\x39\xFC\x36"), 4, &consumed, utf16, 2, &produced) == 0 && consumed == 4 && std::memcmp(utf16, "\x3D\xD8\x00\xDE", 4) == 0); + Require(sceCesGbToUcs2(gb, Bytes("\xA2\xE3"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x20AC); + + std::uint8_t mbcs[4] = {1, 2, 3, 4}; + produced = 0x55; + Require(sceCesUtf32ToGb(0x10000, gb, mbcs, 3, &produced) == dstBufferEnd && produced == 0x55 && std::memcmp(mbcs, "\x01\x02\x03\x04", 4) == 0); + Require(sceCesUtf32ToGb(0xD800, gb, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUtf32ToGb(0x110000, gb, mbcs, sizeof(mbcs), &produced) == invalidEncode); + Require(sceCesUcs2ToGb(0x80, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 4 && std::memcmp(mbcs, "\x81\x30\x81\x30", 4) == 0); + Require(sceCesUcs2ToGb(0x20AC, gb, mbcs, sizeof(mbcs), &produced) == 0 && produced == 2 && std::memcmp(mbcs, "\xA2\xE3", 2) == 0); + Require(sceCesUtf8ToGb(Bytes("\xF0\x9F\x98\x80"), 4, &consumed, gb, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 4 && produced == 4); + Require(std::memcmp(mbcs, "\x94\x39\xFC\x36", 4) == 0); + const std::uint16_t pair[2] = {0xD83D, 0xDE00}; + Require(sceCesUtf16ToGb(pair, 2, &consumed, gb, mbcs, sizeof(mbcs), &produced) == 0 && consumed == 2 && produced == 4 && std::memcmp(mbcs, "\x94\x39\xFC\x36", 4) == 0); + Require(sceCesUtf16ToGb(pair, 1, &consumed, gb, mbcs, sizeof(mbcs), &produced) == srcBufferEnd); + + alignas(8) std::uint8_t context[64] = {}; + Require(sceCesMbcsUcsContextInit(context, gb) == 0); + const char* utf8Text = "A\xE4\xB8\xAD\xC2\x80\xF0\x9F\x98\x80\xEF\xB8\x90"; + const char* gbText = revision2005 ? "A\xD6\xD0\x81\x30\x81\x30\x94\x39\xFC\x36\xA6\xD9" : "A\xD6\xD0\x81\x30\x81\x30\x94\x39\xFC\x36\x84\x31\x82\x36"; + const std::uint32_t gbSize = revision2005 ? 13 : 15; + std::uint8_t text[16] = {}; + Require(sceCesUtf8StrToMbcsStr(context, Bytes(utf8Text), 13, &consumed, text, sizeof(text), &produced) == 0 && consumed == 13 && produced == gbSize); + Require(std::memcmp(text, gbText, gbSize) == 0); + std::uint8_t backUtf8[16] = {}; + Require(sceCesMbcsStrToUtf8Str(context, text, gbSize, &consumed, backUtf8, sizeof(backUtf8), &produced) == 0 && consumed == gbSize && produced == 13); + Require(std::memcmp(backUtf8, utf8Text, 13) == 0); + std::uint16_t backUtf16[8] = {}; + Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, backUtf16, 8, &produced) == 0 && consumed == gbSize && produced == 6); + Require(backUtf16[0] == 'A' && backUtf16[1] == 0x4E2D && backUtf16[2] == 0x80 && backUtf16[3] == 0xD83D && backUtf16[4] == 0xDE00 && backUtf16[5] == 0xFE10); + Require(sceCesMbcsStrToUtf16Str(context, text, gbSize, &consumed, backUtf16, 4, &produced) == dstBufferEnd); + Require(sceCesMbcsStrGetUtf16Len(context, text, gbSize, &consumed, &produced) == 0 && consumed == gbSize && produced == 6); + std::uint8_t again[16] = {}; + Require(sceCesUtf16StrToMbcsStr(context, backUtf16, 6, &consumed, again, sizeof(again), &produced) == 0 && consumed == 6 && produced == gbSize); + Require(std::memcmp(again, gbText, gbSize) == 0); + std::uint16_t ucs2Text[8] = {}; + Require(sceCesMbcsStrToUcs2Str(context, text, gbSize, &consumed, ucs2Text, 8, &produced) == unassignedCode); + Require(sceCesUtf8StrToMbcsStr(context, Bytes("\xC2\x80"), 2, &consumed, text, 3, &produced) == dstBufferEnd); +} + +int main() { alignas(8) std::uint8_t eucSheet[64] = {}; alignas(8) std::uint8_t gbkSheet[64] = {}; alignas(8) std::uint8_t sjisSheet[64] = {}; @@ -134,5 +367,16 @@ int main() { std::uint8_t back[16] = {}; Require(sceCesMbcsStrToUtf8Str(context, text, produced, &consumed, back, sizeof(back), &produced) == 0); Require(consumed == 4 && produced == 7 && std::memcmp(back, "A\xE4\xB8\xAD\xE2\x82\xAC", 7) == 0); + + alignas(8) std::uint8_t gb2000Sheet[64] = {}; + alignas(8) std::uint8_t gb2005Sheet[64] = {}; + const void* gb2000 = sceCesUcsProfileInitGb18030_2000(gb2000Sheet); + const void* gb2005 = sceCesUcsProfileInitGb18030_2005(gb2005Sheet); + Require(gb2000 == gb2000Sheet && gb2005 == gb2005Sheet); + Require(sceCesUcsProfileInitGb18030_2000(nullptr) == nullptr && sceCesUcsProfileInitGb18030_2005(nullptr) == nullptr); + Require(sceCesEucCnToUcs2(gb2000, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == invalidParameter); + Require(sceCesGbToUcs2(gb2005, Bytes("\xD6\xD0"), 2, &consumed, &ucs2) == 0 && consumed == 2 && ucs2 == 0x4E2D); + CheckGb18030(gb2000, false, 0xE61D3BD3, 0x83476043, 0x469CFCE5, 0, 0); + CheckGb18030(gb2005, true, 0x83EDDA69, 0x8D188DFC, 0x31CD0B55, 18, 24); return 0; } diff --git a/docs/dev/debt/libSceCesCs.md b/docs/dev/debt/libSceCesCs.md index 6c90ed380d..f0af2576a1 100644 --- a/docs/dev/debt/libSceCesCs.md +++ b/docs/dev/debt/libSceCesCs.md @@ -13,9 +13,10 @@ - [Shift-JIS and EUC-JP scalar conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSJisTo*`, `sceCesEucJpTo{Utf16,Utf32}*` and `sceCes{Ucs2,Utf8,Utf16,Utf32}*To{SJis,EucJp}` are unverified against the console. Signatures follow the Orbital type database linked above; the CP932 and CP51932 table and profile assumptions above apply. A profile of another encoding answers `INVALID_PARAMETER`, a surrogate or a value above U+10FFFF `INVALID_ENCODE`, a code point outside the BMP `UNASSIGNED_CODE`. `sceCesSJisCodeToJisX0208` and `sceCesJisX0208ToSJisCode` do not check that the cell is assigned; their `INVALID_ENCODE` for rows or cells outside 1-94 and `INVALID_DST_BUFFER` for a null output are not console measurements - [Strict JIS X 0208 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - cell `0x2140` and U+FF3C throw: JIS0208.TXT gives U+005C, glibc 2.39 and Python's `euc_jp` U+FF3C. In `sceCesUcsProfileInitSJis1997X0208`, bytes `0x5C` and `0x7E` and U+005C, U+007E, U+00A5 and U+203E throw: SHIFTJIS.TXT gives JIS X 0201 Roman, CP932 and Python's `shift_jis` ASCII; the console's is unknown. The table, [JIS0208.TXT](https://unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0208.TXT), is CP932's without rows 13 and 89-92 and with six cells changed, not measured. Per [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Text/Ces.cs#L57-L67), the `X0208Ss2` profile adds SS2 katakana and `X0208Ss2Ss3` SS3 JIS X 0212. Signatures follow Orbital; the conversions above take these profiles with the same errors, a missing SS2 or SS3 answering `INVALID_ENCODE` and Shift-JIS leads `0xF0`-`0xFC` `UNASSIGNED_CODE` - [UTF-16 and UTF-32 forms of the single-byte and MBCS string conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - errors, their precedence and the optional length pointers of `sceCesSbcTo{Ucs2,Utf16,Utf32}*`, `sceCes{Ucs2,Utf16,Utf32}*ToSbc`, `sceCesMbcsStr{To,Get}{Utf16,Utf32}*` and `sceCes{Utf16,Utf32}Str{To,Get}Mbcs*` are unverified against the console. Signatures follow the Orbital type database linked above. They follow `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the UTF-8 MBCS string forms with the profiles and tables listed above. A surrogate or a value above U+10FFFF answers `INVALID_ENCODE`, a valid code point no profile maps `UNASSIGNED_CODE`. The string forms stop at a zero code unit, write no terminator and count code units -- [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas, GB 18030 four-byte forms and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified +- [GBK (CP936) and EUC-CN conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesUcsProfileInitGbkCp936` and `sceCesUcsProfileInitEucCnGb2312` mark the caller's sheet with an 8-byte tag; the console's profile layout is not known. Both use one table ([Cp936.hpp](../../../core/libs/prx/libSceCesCs/Cp936.hpp)) read from Microsoft's [CP936.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP936.TXT); the EUC-CN profile accepts only its `0xA1`-`0xFE` lead and trail bytes, so it includes the GBK cells inside that range and the CP936 forms of `0xA1A4` (U+00B7) and `0xA1AA` (U+2014) instead of GB 2312's. That the console's GB 2312 and CP936 tables equal it is not measured. User-defined areas and `sceCesGbUcsProfileSet*` are not handled. Errors, their precedence and the length pointers follow the Shift-JIS forms above and are unverified +- [GB 18030 profiles](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the console's tables are not measured, and the sources disagree on 26 sequences. The 2000 profile follows ICU's `gb-18030-2000.ucm` at icu-data d9d3a6ed (before ICU-8427), which matches Python's codec: `0xA8BC` is U+E7C7 and `0x8135F437` U+1E3F. The 2005 profile follows `gb-18030-2005.ucm` (91850aec), which swaps those two and maps 24 PUA cells to U+FE10-U+FE19, U+9FB4-U+9FBB and six supplementary ideographs, keeping the PUA code points as encode fallbacks. Two-byte cells are CP936 plus [Gb18030.hpp](../../../core/libs/prx/libSceCesCs/Gb18030.hpp); four-byte runs include ICU's `gb18030Ranges`. The inits tag the sheet like GBK's; the `sceCesGb*`, `sceCesMbc*` and MBCS string forms accept them. The full mapping is assumed as the default of `sceCesGbUcsProfileSetUdaMapping` and `Set4ByteCharRange`, which throw. Supplementary characters to UCS-2 answer `UNASSIGNED_CODE`; other errors follow the GBK forms - [sceCesRefersUcsProfileCp1250](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with the Windows-1250 table of Microsoft's [bestfit1250.txt](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/bestfit1250.txt), where the unassigned bytes map to their Latin-1 control characters as the Cp1252 and Cp1251 tables do; the console behavior for those bytes and the whole profile is not verified. PPSA34501 fails to start without this export (`qYu6y1E0Qbg`) - [Generic MBCS character and Shift-JIS code helpers](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - `sceCesMbcTo*` convert one character with any profile the MBCS context accepts, as the per-encoding forms above do. `sceCesSJisGetCode` and `sceCesSJisPutCode` read and write one raw Shift-JIS code (one byte for ASCII and `0xA1`-`0xDF`) without checking that it is assigned. `sceCesSJisCodeToJisX0213` and `sceCesJisX0213ToSJisCode` use the Shift_JIS-2004 arithmetic of JIS X 0213 annex 1 (plane 2 rows 1, 3-5, 8, 12-15 and 78-94 on leads `0xF0`-`0xFC`). `sceCesMbcsUcsContextInitCopy` copies the stored profile pointer. Signatures follow the Orbital type database linked above; errors, their precedence and the optional size pointers follow the other converters and are unverified against the console - [Single-byte profiles](../../../core/libs/prx/libSceCesCs/SingleByte.hpp) (libSceCesCs) - the console's tables for these 27 profiles are not verified. Each returns a profile accepted by `sceCesSbcToUtf8`, `sceCesUtf8ToSbc` and the MBCS context conversions, with bytes 0x00-0x7F unchanged and the high half of a public table: ISO 8859-1, -2, -4, -5, -9, -10, -13, -14, -15 and -16 from [unicode.org](https://unicode.org/Public/MAPPINGS/ISO8859/), code pages 437, 737, 775, 850, 852, 855, 860-863, 865 and 866 from [Microsoft's PC tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/PC/), KOI8-R and KOI8-U from [unicode.org](https://unicode.org/Public/MAPPINGS/VENDORS/MISC/), and 1254, 1256 and 1258 from Microsoft's [best-fit tables](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WindowsBestFit/), whose unassigned bytes map to C1 controls as in Cp1250. Profiles whose public table leaves bytes undefined or maps them to private use still throw -- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 72 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. +- [libSceCesCs firmware 3.20 pending exports](../../../core/libs/prx/libSceCesCs/PendingExports.cpp) - 70 symbol names are present in [the PS5 3.20 libSceCesCs export list](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceCesCs.c). Their signatures and console behaviour are unverified. The declarations use the existing no-parameter unknown-signature convention solely to resolve imports, and every call throws through `NotImplemented_nid_no_patch`. - [EUC-KR, UHC and Big5 conversions](../../../core/libs/prx/libSceCesCs/Export.cpp) (libSceCesCs) - the tables, errors and Big5 duplicate choices are assumed, not measured. `sceCesUcsProfileInitEucKr`, `sceCesUcsProfileInitUhc` and `sceCesUcsProfileInitBig5Cp950` tag the caller's sheet like the EUC-JP profile. The tables are Microsoft's [CP949.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP949.TXT) and [CP950.TXT](https://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP950.TXT); EUC-KR is CP949 limited to bytes `0xA1`-`0xFE`. Bytes below `0x80` are ASCII; the KS X 1003 profile (`sceCes*UcsProfileSetSbcs`) is not implemented. User-defined areas (Private Use Area on Windows) answer `UNASSIGNED_CODE`. Big5 encoding takes the lower of two cells except for U+2550, U+255E, U+2561, U+256A, U+5341 and U+5345, as code page 950 does. The scalar calls to and from UCS-2 and UTF-8/16/32 convert one character as the Shift-JIS and EUC-JP scalar conversions do From 3654f15bfaf40914956bccc430bca3aa7fc593d4 Mon Sep 17 00:00:00 2001 From: thibaut Date: Wed, 21 Oct 2026 19:55:54 +0200 Subject: [PATCH 313/318] feat(shader): bit-exact RDNA v_rcp_f32 behind APS5_EXACT_RCP v_rcp_f32 and v_rcp_iflag_f32 lower to the driver's 1/x, whose low bits differ from the console's on NVIDIA and Intel. With APS5_EXACT_RCP set, normal inputs with normal results take the RDNA reciprocal datapath instead: a cubic over 32 segments of the mantissa in 32-bit integer arithmetic, the linear term rounded to nearest even, and a 2,761-entry table of one-ULP corrections, looked up only when the dropped bits fall next to the rounding cut (6.25 % of inputs). This reproduces all 2^23 mantissas of an RDNA2 (gfx1031) sweep. Other inputs keep 1/x. It is off by default: the tables add about 13k SPIR-V words to each shader that uses it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017EjjFU1FjMR11oWDYs8UMo --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 + .../tests/execution/RcpHardware.cpp | 162 ++++++ core/shader/recompiler/CMakeLists.txt | 1 + .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 2 + .../src/SpirvAlu/SpirvAluEmitterMath.cpp | 8 - .../SpirvAlu/SpirvAluEmitterReciprocal.cpp | 502 ++++++++++++++++++ .../SpirvBackend/src/SpirvModuleEmitter.cpp | 1 + docs/dev/ENVIRONMENT.md | 1 + docs/dev/debt/shader.md | 1 + 10 files changed, 677 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/RcpHardware.cpp create mode 100644 core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 75f7e48d00..e2f14d2d93 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2128,6 +2128,12 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_rcp_hardware_exact COMMAND agc_driver_rcp_hardware_tests) + set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT "APS5_EXACT_RCP=1;ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 60) + if(WIN32) + set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS) if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/RcpHardware.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/RcpHardware.cpp new file mode 100644 index 0000000000..66a3c78357 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/RcpHardware.cpp @@ -0,0 +1,162 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::array, 256> Measured{{ + {0x000000u, 0x3f800000u}, {0x002922u, 0x3f7fadd6u}, {0x00a1a9u, 0x3f7ebe44u}, {0x010c47u, 0x3f7debceu}, + {0x013df5u, 0x3f7d8a32u}, {0x01a87fu, 0x3f7cb9ddu}, {0x024f43u, 0x3f7b766fu}, {0x02d76du, 0x3f7a70beu}, + {0x031690u, 0x3f79f820u}, {0x033035u, 0x3f79c742u}, {0x03adabu, 0x3f78d946u}, {0x04435bu, 0x3f77bfa4u}, + {0x04f3a1u, 0x3f76772au}, {0x057a96u, 0x3f757df8u}, {0x05c6afu, 0x3f74f253u}, {0x06342au, 0x3f742a80u}, + {0x06bf6fu, 0x3f732e24u}, {0x07709au, 0x3f71f00au}, {0x07eb02u, 0x3f711627u}, {0x086273u, 0x3f704304u}, + {0x09362bu, 0x3f6ed04au}, {0x099950u, 0x3f6e2438u}, {0x09f75cu, 0x3f6d81e2u}, {0x0a097cu, 0x3f6d62b3u}, + {0x0ab7e3u, 0x3f6c3840u}, {0x0b1a69u, 0x3f6b90f0u}, {0x0b93a5u, 0x3f6ac454u}, {0x0becd7u, 0x3f6a2eadu}, + {0x0bf0b4u, 0x3f6a2836u}, {0x0c5c7fu, 0x3f697463u}, {0x0c9164u, 0x3f691c8au}, {0x0cb1e2u, 0x3f68e6b5u}, + {0x0d5f89u, 0x3f67c8a0u}, {0x0dd27au, 0x3f670cc6u}, {0x0e09e4u, 0x3f66b2a2u}, {0x0ead02u, 0x3f65aae2u}, + {0x0ed904u, 0x3f656421u}, {0x0f17a3u, 0x3f64ffbeu}, {0x0f21ddu, 0x3f64ef61u}, {0x0f2bcdu, 0x3f64df7du}, + {0x0f4205u, 0x3f64bbfeu}, {0x0f8808u, 0x3f644c6cu}, {0x0faddcu, 0x3f641050u}, {0x0fd630u, 0x3f63d05fu}, + {0x1012f0u, 0x3f637050u}, {0x108fe7u, 0x3f62abb4u}, {0x10a3d6u, 0x3f628c77u}, {0x1184b9u, 0x3f612e5au}, + {0x119a72u, 0x3f610cc2u}, {0x11e20bu, 0x3f609e4fu}, {0x123f59u, 0x3f600f00u}, {0x128b2fu, 0x3f5f9b0du}, + {0x12bd4au, 0x3f5f4eb3u}, {0x12f18du, 0x3f5eff48u}, {0x135d16u, 0x3f5e5c8du}, {0x13deefu, 0x3f5d994bu}, + {0x140ceeu, 0x3f5d5472u}, {0x14a0f9u, 0x3f5c77fdu}, {0x14f473u, 0x3f5bfc70u}, {0x15051au, 0x3f5be3dau}, + {0x153e7cu, 0x3f5b8f4fu}, {0x15b949u, 0x3f5adb3au}, {0x1600a3u, 0x3f5a7320u}, {0x1676ebu, 0x3f59c766u}, + {0x172abbu, 0x3f58c45au}, {0x1738f7u, 0x3f58aff3u}, {0x17a05au, 0x3f581c33u}, {0x17f5e8u, 0x3f57a288u}, + {0x1818e8u, 0x3f5770e9u}, {0x185f1au, 0x3f570da8u}, {0x18f135u, 0x3f564038u}, {0x194f2fu, 0x3f55bce2u}, + {0x1a61dbu, 0x3f54409cu}, {0x1a63e2u, 0x3f543dd2u}, {0x1af098u, 0x3f537d12u}, {0x1bd958u, 0x3f52413au}, + {0x1cb224u, 0x3f511e54u}, {0x1deb7au, 0x3f4f7f68u}, {0x1df9fdu, 0x3f4f6c59u}, {0x1e27a1u, 0x3f4f307du}, + {0x1e398fu, 0x3f4f1903u}, {0x1ef9d8u, 0x3f4e1e85u}, {0x1fb17cu, 0x3f4d317eu}, {0x1fec12u, 0x3f4ce652u}, + {0x202036u, 0x3f4ca39au}, {0x20b671u, 0x3f4be450u}, {0x211c70u, 0x3f4b633bu}, {0x21b455u, 0x3f4aa42eu}, + {0x2217beu, 0x3f4a27e7u}, {0x22c947u, 0x3f494b6eu}, {0x230d97u, 0x3f48f719u}, {0x23a75fu, 0x3f483a41u}, + {0x244f6fu, 0x3f476d74u}, {0x24ede6u, 0x3f46add8u}, {0x250d33u, 0x3f46882au}, {0x254b0cu, 0x3f463de2u}, + {0x263ba6u, 0x3f451ef3u}, {0x269e0du, 0x3f44aa89u}, {0x26a2c0u, 0x3f44a4fdu}, {0x26bb7du, 0x3f4487d0u}, + {0x26e875u, 0x3f4452ddu}, {0x2744e3u, 0x3f43e660u}, {0x281972u, 0x3f42eeaau}, {0x29b882u, 0x3f4111f2u}, + {0x2a3af4u, 0x3f407e00u}, {0x2a96fbu, 0x3f401628u}, {0x2afa95u, 0x3f3fa642u}, {0x2b0537u, 0x3f3f9a58u}, + {0x2baf6au, 0x3f3edc66u}, {0x2c7557u, 0x3f3e015au}, {0x2d1c9au, 0x3f3d49c5u}, {0x2d711au, 0x3f3ced8cu}, + {0x2e0531u, 0x3f3c4cc6u}, {0x2e4415u, 0x3f3c08d1u}, {0x2e8e7du, 0x3f3bb8aau}, {0x2eae05u, 0x3f3b96c8u}, + {0x2fa54eu, 0x3f3a8eaeu}, {0x301850u, 0x3f3a14d7u}, {0x30d16au, 0x3f39520au}, {0x31f5efu, 0x3f38216cu}, + {0x32c6ecu, 0x3f374a2du}, {0x33d0ceu, 0x3f363b28u}, {0x34b9b5u, 0x3f355051u}, {0x34c9ebu, 0x3f35400eu}, + {0x364b9du, 0x3f33c092u}, {0x36f675u, 0x3f3318bau}, {0x376cbfu, 0x3f32a53au}, {0x37dc76u, 0x3f3238afu}, + {0x387a34u, 0x3f31a04au}, {0x3898d1u, 0x3f3182d5u}, {0x392630u, 0x3f30fb4bu}, {0x398406u, 0x3f30a1c6u}, + {0x3aa8b5u, 0x3f2f8cd0u}, {0x3b1287u, 0x3f2f2983u}, {0x3b6186u, 0x3f2edfabu}, {0x3bbbe9u, 0x3f2e8b79u}, + {0x3bfb1eu, 0x3f2e50c8u}, {0x3cf38cu, 0x3f2d6b98u}, {0x3d9c17u, 0x3f2cd172u}, {0x3e7d1bu, 0x3f2c054du}, + {0x3e875fu, 0x3f2bfc08u}, {0x3f63afu, 0x3f2b360fu}, {0x3f98e2u, 0x3f2b0685u}, {0x3fa7d9u, 0x3f2af92au}, + {0x407b49u, 0x3f2a3d5au}, {0x4174afu, 0x3f2961e2u}, {0x427df9u, 0x3f287ad8u}, {0x43435cu, 0x3f27d089u}, + {0x437bcbu, 0x3f27a016u}, {0x44985eu, 0x3f26ad72u}, {0x451abdu, 0x3f263f34u}, {0x457d42u, 0x3f25ec44u}, + {0x472077u, 0x3f248ef6u}, {0x4724dau, 0x3f248b56u}, {0x47469au, 0x3f246f78u}, {0x482c9cu, 0x3f23b287u}, + {0x486824u, 0x3f2381e6u}, {0x48db40u, 0x3f232432u}, {0x499523u, 0x3f228dc2u}, {0x49b64au, 0x3f22730au}, + {0x49dea9u, 0x3f22528du}, {0x4a23d5u, 0x3f221b02u}, {0x4b14f9u, 0x3f215a86u}, {0x4c25fcu, 0x3f2082beu}, + {0x4cbd87u, 0x3f200befu}, {0x4cdd20u, 0x3f1ff340u}, {0x4dd422u, 0x3f1f334cu}, {0x4ef8aau, 0x3f1e5249u}, + {0x4f20f3u, 0x3f1e337eu}, {0x4f426du, 0x3f1e19f1u}, {0x5051c1u, 0x3f1d4c05u}, {0x506bf2u, 0x3f1d3841u}, + {0x507313u, 0x3f1d32e0u}, {0x519088u, 0x3f1c5cc0u}, {0x520935u, 0x3f1c02eau}, {0x52e6b4u, 0x3f1b5f10u}, + {0x536d35u, 0x3f1afc38u}, {0x54a6d8u, 0x3f1a17a2u}, {0x55c663u, 0x3f19485eu}, {0x56ed5bu, 0x3f187600u}, + {0x571242u, 0x3f185bd8u}, {0x5790f8u, 0x3f180249u}, {0x57ee05u, 0x3f17c0c8u}, {0x588a7eu, 0x3f175320u}, + {0x58d556u, 0x3f171ee4u}, {0x59a54au, 0x3f168e80u}, {0x59f999u, 0x3f165444u}, {0x5affb2u, 0x3f15a05bu}, + {0x5b855cu, 0x3f15453fu}, {0x5bd86du, 0x3f150cd9u}, {0x5c90a9u, 0x3f149059u}, {0x5cd546u, 0x3f146230u}, + {0x5d9dc9u, 0x3f13dbf0u}, {0x5e14d9u, 0x3f138caau}, {0x5e8766u, 0x3f1340b6u}, {0x5f41cau, 0x3f12c5c6u}, + {0x5f5572u, 0x3f12b8dcu}, {0x60fd9bu, 0x3f11a440u}, {0x616499u, 0x3f1161b4u}, {0x625d1cu, 0x3f10c218u}, + {0x62c33au, 0x3f1080e8u}, {0x63c0e3u, 0x3f0fdff7u}, {0x641547u, 0x3f0faabcu}, {0x64b8d7u, 0x3f0f43feu}, + {0x651327u, 0x3f0f0b83u}, {0x658cdau, 0x3f0ebfadu}, {0x65dc9fu, 0x3f0e8e23u}, {0x662b59u, 0x3f0e5d60u}, + {0x66d228u, 0x3f0df67eu}, {0x670f27u, 0x3f0dd104u}, {0x67e4c4u, 0x3f0d4e61u}, {0x6916adu, 0x3f0c94eeu}, + {0x6a50dfu, 0x3f0bd86cu}, {0x6a7a5au, 0x3f0bbfaeu}, {0x6b0a18u, 0x3f0b6a37u}, {0x6b0d54u, 0x3f0b684cu}, + {0x6b4013u, 0x3f0b4a3au}, {0x6b4cb2u, 0x3f0b42c1u}, {0x6bd361u, 0x3f0af338u}, {0x6bf46cu, 0x3f0adfc3u}, + {0x6cad4au, 0x3f0a734au}, {0x6cb93du, 0x3f0a6c4cu}, {0x6d76b0u, 0x3f09fdddu}, {0x6dc676u, 0x3f09cf92u}, + {0x6e36aau, 0x3f098ea8u}, {0x6f0367u, 0x3f0918d3u}, {0x6fa5cau, 0x3f08bbedu}, {0x70c958u, 0x3f08165eu}, + {0x71e6c2u, 0x3f0775ccu}, {0x721583u, 0x3f075ba3u}, {0x72e6ccu, 0x3f06e703u}, {0x72fdf2u, 0x3f06da29u}, + {0x73596du, 0x3f06a778u}, {0x7403e4u, 0x3f064966u}, {0x744916u, 0x3f06235cu}, {0x74c9dfu, 0x3f05dccau}, + {0x74e69au, 0x3f05cd16u}, {0x756dbau, 0x3f05836cu}, {0x7631a9u, 0x3f05192au}, {0x7731afu, 0x3f048f50u}, + {0x7774a2u, 0x3f046b72u}, {0x791a7cu, 0x3f038b32u}, {0x795e82u, 0x3f036751u}, {0x7aa2c1u, 0x3f02bd52u}, + {0x7c2684u, 0x3f01f444u}, {0x7c64ebu, 0x3f01d422u}, {0x7d2cafu, 0x3f016db2u}, {0x7d959fu, 0x3f013822u}, + {0x7e62aau, 0x3f00cffbu}, {0x7ebff2u, 0x3f00a0d0u}, {0x7eecf4u, 0x3f008a1au}, {0x7f1505u, 0x3f0075eau}, + {0x7f1b10u, 0x3f0072dfu}, {0x7f2614u, 0x3f006d53u}, {0x7ff486u, 0x3f0005beu}, {0x7fffffu, 0x3f000001u}, +}}; +constexpr std::array Exponents{1u, 60u, 127u, 128u, 200u, 252u}; +constexpr std::uint32_t Cases = static_cast(Measured.size() * Exponents.size() * 2u); +constexpr std::uint32_t Batches = Cases / Threads; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020082, 0xe0301000, 0x80000201, 0xbf8c3f70, 0x7e065502, 0xe0701000, 0x80010301, 0xbf8c3f70, + 0xbf810000, +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((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; +} + +std::uint32_t Sample(std::uint32_t index) { + const auto& measured = Measured[index % Measured.size()]; + const auto exponent = Exponents[(index / Measured.size()) % Exponents.size()]; + const auto sign = (index / static_cast(Measured.size() * Exponents.size())) << 31u; + return sign | exponent << 23u | measured[0]; +} + +std::uint32_t Expected(std::uint32_t index) { + const auto& measured = Measured[index % Measured.size()]; + const auto exponent = Exponents[(index / Measured.size()) % Exponents.size()]; + const auto sign = (index / static_cast(Measured.size() * Exponents.size())) << 31u; + return sign | ((measured[1] >> 23u) + 127u - exponent) << 23u | (measured[1] & 0x7fffffu); +} +} + +int main() { + try { + if (std::getenv("APS5_EXACT_RCP") == nullptr) return VulkanTestSkipped; + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {32, 0, userData, compute, std::nullopt, std::nullopt, memory}, + device->Target(), + {0, 0, 0, 128} + }; + request.useCache = false; + request.context.floatMode = ShaderRecompiler::ShaderFloatMode{0xf0u, true, true, false}; + const auto result = ShaderRecompiler::Recompile(request); + for (std::uint32_t batch = 0; batch < Batches; ++batch) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Input[tid] = Sample(batch * Threads + tid); + Output.fill(0xdeadbeefu); + device->Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device->WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const auto wanted = Expected(batch * Threads + tid); + Require(Output[tid] == wanted, std::string("v_rcp_f32 of ") + Hex(Input[tid]) + " is " + Hex(Output[tid]) + ", expected the RDNA 2 value " + Hex(wanted)); + } + } + std::puts("rcp hardware tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index b278d2b8a0..2f8d98958f 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -131,6 +131,7 @@ function(add_shader_recompiler target runtimeTarget) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvAnalysis.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvAlu/SpirvAluEmitterRegisters.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvFlowEmitter.cpp ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/SpirvBackend/src/SpirvImageEmitter.cpp diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 34213a41b6..6556b78469 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -154,6 +154,7 @@ std::uint32_t EmitF16BitsToF32Decoded(SpirvEmitterState& state, std::uint32_t bi void EmitProgram(SpirvEmitterState& state); void DefineGetBdaPointer(SpirvEmitterState& state); void DefineDot2F32F16Function(SpirvEmitterState& state); +void DefineReciprocalCorrectionFunction(SpirvEmitterState& state); void EmitLabel(SpirvEmitterState& state, std::uint32_t label); std::uint32_t Unary(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t type, std::uint32_t value); std::uint32_t Binary(SpirvEmitterState& state, std::uint32_t opcode, std::uint32_t type, std::uint32_t lhs, std::uint32_t rhs); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 38b321e595..16541a653a 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -167,6 +167,8 @@ struct SpirvEmitterState { bool nativeF16ModesEmitted = false; bool f16DenormPreserveEmitted = false; std::uint32_t dot2F32F16Function = 0; + std::uint32_t reciprocalCorrectionFunction = 0; + std::array reciprocalTables {}; std::map, std::uint32_t> interpolateFunctions; bool integerInterpolation = false; std::array, 2> bdaDwordReadFunctions {}; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index 1ce65a7506..abf7b6fce4 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -983,14 +983,6 @@ std::uint32_t EmitFPMedTri32(SpirvEmitterState& state, std::uint32_t arg0, std:: return value(Select(state, u32, anyNan, min3, median)); } -std::uint32_t EmitFPRecip32(SpirvEmitterState& state, std::uint32_t arg0) { - return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), arg0); -} - -std::uint32_t EmitFPRecipIFlag32(SpirvEmitterState& state, std::uint32_t arg0) { - return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), arg0); -} - std::uint32_t EmitFPRecipSqrt32(SpirvEmitterState& state, std::uint32_t arg0) { return EmitExt(state, TypeF32(state), GLSLstd450InverseSqrt, {arg0}); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp new file mode 100644 index 0000000000..69a21a884c --- /dev/null +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp @@ -0,0 +1,502 @@ +#include "SpirvBackend/SpirvEmitterHelpers.hpp" +#include "SpirvBackend/SpirvEmitterInstructions.hpp" +#include "IntermediateRepresentation/IrBlock.hpp" +#include "IntermediateRepresentation/IrValue.hpp" +#include +#include +#include +#include +#include + +namespace ShaderRecompiler { +namespace { + +constexpr std::array ReciprocalConstant{ + 0x04000000u, 0x03e0f83eu, 0x03c3c3c3u, 0x03a83a83u, 0x038e38e3u, 0x03759f22u, 0x035e50d7u, 0x03483483u, + 0x03333333u, 0x031f3831u, 0x030c30c3u, 0x02fa0be8u, 0x02e8ba2eu, 0x02d82d82u, 0x02c8590bu, 0x02b93105u, + 0x02aaaaaau, 0x029cbc14u, 0x028f5c28u, 0x02828282u, 0x02762762u, 0x026a439fu, 0x025ed097u, 0x0253c825u, + 0x02492492u, 0x023ee08fu, 0x0234f72cu, 0x022b63cbu, 0x02222222u, 0x02192e29u, 0x02108421u, 0x02082082u, +}; + +constexpr std::array ReciprocalLinear{ + 0x001ffff2u, 0x001e16feu, 0x001c588au, 0x001abfcfu, 0x001948a9u, 0x0017ef84u, 0x0016b143u, 0x00158b2cu, + 0x00147adcu, 0x00137e3bu, 0x0012936eu, 0x0011b8d2u, 0x0010ecf2u, 0x00102e83u, 0x000f7c5cu, 0x000ed575u, + 0x000e38e1u, 0x000da5cau, 0x000d1b6fu, 0x000c9924u, 0x000c1e4au, 0x000baa54u, 0x000b3cbfu, 0x000ad517u, + 0x000a72efu, 0x000a15e6u, 0x0009bda3u, 0x000969d3u, 0x00091a2au, 0x0008ce64u, 0x00088642u, 0x00084187u, +}; + +constexpr std::array ReciprocalHigherOrder{ + 0x0785ffb6u, 0x06a9e92du, 0x05ecd536u, 0x0548c377u, 0x04bab3a2u, 0x043ea577u, 0x03d198c0u, 0x03728d4eu, + 0x031e82f9u, 0x02d479a0u, 0x02927126u, 0x02576970u, 0x0223626au, 0x01f55c00u, 0x01cb5622u, 0x01a550c1u, + 0x01844bd0u, 0x01654744u, 0x014a4313u, 0x01313f35u, 0x011a3ba2u, 0x01063852u, 0x00f33540u, 0x00e23266u, + 0x00d22fbfu, 0x00c42d47u, 0x00b72afau, 0x00ab28d4u, 0x00a026d2u, 0x009624f2u, 0x008c232fu, 0x00832189u, +}; + +constexpr std::array ReciprocalCorrectionStarts{ + 0x00000000u, 0x00020000u, 0x00040002u, 0x000a0008u, 0x0012000cu, 0x00160014u, 0x001a0018u, 0x0023001cu, + 0x00280026u, 0x00290029u, 0x002f002cu, 0x00320031u, 0x00390038u, 0x0042003eu, 0x00480045u, 0x004a004au, + 0x004f004du, 0x00550052u, 0x00570056u, 0x005d005bu, 0x005f005eu, 0x00640061u, 0x006b0069u, 0x0076006fu, + 0x007d0077u, 0x0082007eu, 0x00850084u, 0x008d0088u, 0x00920090u, 0x00990097u, 0x00a0009cu, 0x00a400a3u, + 0x00ab00a8u, 0x00ab00abu, 0x00b000adu, 0x00b700b5u, 0x00bb00b8u, 0x00be00bdu, 0x00c100c1u, 0x00c300c3u, + 0x00c900c5u, 0x00d000cdu, 0x00d300d2u, 0x00d800d5u, 0x00db00dau, 0x00de00dcu, 0x00e100dfu, 0x00e900e3u, + 0x00ea00e9u, 0x00ef00edu, 0x00f300f1u, 0x00fb00f7u, 0x00fb00fbu, 0x00ff00feu, 0x01060105u, 0x01080106u, + 0x010c010au, 0x0112010fu, 0x01150112u, 0x011a0117u, 0x011c011bu, 0x01240120u, 0x012b0126u, 0x0132012fu, + 0x01360135u, 0x013b0137u, 0x013f013du, 0x01490143u, 0x014a0149u, 0x014e014du, 0x014f014fu, 0x01540151u, + 0x01580158u, 0x015b0158u, 0x0161015du, 0x01650163u, 0x01670166u, 0x016a0169u, 0x016d016bu, 0x0170016eu, + 0x01730173u, 0x01750175u, 0x017b0179u, 0x01830181u, 0x01860184u, 0x01870187u, 0x018c018au, 0x0191018fu, + 0x01970194u, 0x01a1019eu, 0x01a601a4u, 0x01ab01a8u, 0x01b001acu, 0x01b701b5u, 0x01c101bfu, 0x01c701c4u, + 0x01cd01cbu, 0x01d001cfu, 0x01d501d4u, 0x01d701d6u, 0x01de01ddu, 0x01e301e2u, 0x01e601e5u, 0x01eb01eau, + 0x01eb01ebu, 0x01ee01ecu, 0x01f001f0u, 0x01f601f4u, 0x01fa01f7u, 0x01fb01fbu, 0x020101feu, 0x02030201u, + 0x020a0207u, 0x020e020cu, 0x02140212u, 0x02170217u, 0x021d021cu, 0x0220021du, 0x02210221u, 0x02280225u, + 0x0230022bu, 0x02370233u, 0x023b023bu, 0x023f023bu, 0x02470241u, 0x024b0249u, 0x024f024cu, 0x02540250u, + 0x02560255u, 0x025a0257u, 0x025a025au, 0x025f025cu, 0x0261025fu, 0x02660264u, 0x02670267u, 0x026d026bu, + 0x026f026fu, 0x02710271u, 0x02740274u, 0x02760276u, 0x02790276u, 0x027e027cu, 0x0282027fu, 0x02830283u, + 0x02850285u, 0x02880285u, 0x028d028du, 0x028f028eu, 0x0292028fu, 0x02960293u, 0x029b029au, 0x02a0029du, + 0x02aa02a5u, 0x02b302acu, 0x02b602b4u, 0x02b902b8u, 0x02bc02bbu, 0x02be02bdu, 0x02c302c0u, 0x02c902c5u, + 0x02ce02cau, 0x02d202d0u, 0x02d302d2u, 0x02d802d6u, 0x02db02dau, 0x02de02deu, 0x02e102deu, 0x02e102e1u, + 0x02e602e2u, 0x02ec02eau, 0x02f002eeu, 0x02f502f2u, 0x02f602f6u, 0x02f802f7u, 0x02fc02fau, 0x030002fdu, + 0x03040302u, 0x03060304u, 0x030a0309u, 0x030d030cu, 0x0310030fu, 0x03130311u, 0x03160316u, 0x031c0319u, + 0x031f031du, 0x03260322u, 0x0330032au, 0x03350332u, 0x03380337u, 0x033c0339u, 0x0341033du, 0x03470344u, + 0x034b0349u, 0x034d034cu, 0x034d034du, 0x03500350u, 0x03530350u, 0x03570354u, 0x035a0358u, 0x035b035bu, + 0x0361035fu, 0x03620361u, 0x03660363u, 0x03680366u, 0x03690368u, 0x036b036au, 0x036d036cu, 0x0370036eu, + 0x03720371u, 0x03750375u, 0x03790376u, 0x037f037bu, 0x03830382u, 0x03870384u, 0x038a0389u, 0x0390038du, + 0x03950393u, 0x03960395u, 0x03990396u, 0x039e039bu, 0x039f039fu, 0x03a303a2u, 0x03a503a5u, 0x03ab03a8u, + 0x03af03aeu, 0x03b103b0u, 0x03b403b3u, 0x03b603b4u, 0x03b803b8u, 0x03bc03b9u, 0x03c003beu, 0x03c303c0u, + 0x03c503c3u, 0x03c503c5u, 0x03c603c5u, 0x03ca03c8u, 0x03ca03cau, 0x03cc03cbu, 0x03cf03cdu, 0x03d303d0u, + 0x03d503d4u, 0x03d803d7u, 0x03da03d9u, 0x03dc03dbu, 0x03de03dcu, 0x03de03deu, 0x03e203e0u, 0x03e703e5u, + 0x03e903e8u, 0x03ea03eau, 0x03ed03ebu, 0x03ef03eeu, 0x03f103efu, 0x03f503f2u, 0x03fb03f7u, 0x03fe03fbu, + 0x04080401u, 0x040d040bu, 0x040e040eu, 0x040f040eu, 0x04130411u, 0x04130413u, 0x04150414u, 0x04160415u, + 0x04180416u, 0x041b041au, 0x041e041du, 0x0420041fu, 0x04240421u, 0x04280428u, 0x042b042bu, 0x042d042bu, + 0x042f042fu, 0x04330431u, 0x04330433u, 0x04350433u, 0x04380436u, 0x043d043bu, 0x043f043eu, 0x0441043fu, + 0x04450443u, 0x04460445u, 0x04460446u, 0x044a0449u, 0x0450044eu, 0x04560454u, 0x04590457u, 0x045e045bu, + 0x04640462u, 0x04650464u, 0x046b0466u, 0x046c046cu, 0x0470046eu, 0x04750473u, 0x04780477u, 0x047b047au, + 0x0480047eu, 0x04810480u, 0x04830481u, 0x04850483u, 0x04870486u, 0x048b0488u, 0x048f048eu, 0x0490048fu, + 0x04920492u, 0x04940492u, 0x04970497u, 0x049b049au, 0x049f049eu, 0x04a1049fu, 0x04a304a2u, 0x04a504a5u, + 0x04a704a6u, 0x04a904a8u, 0x04ad04abu, 0x04b304afu, 0x04b504b5u, 0x04b604b5u, 0x04b804b7u, 0x04bc04bbu, + 0x04c104beu, 0x04c204c2u, 0x04c404c3u, 0x04c404c4u, 0x04c504c5u, 0x04c704c6u, 0x04c804c7u, 0x04c904c9u, + 0x04c904c9u, 0x04ce04cdu, 0x04d004cfu, 0x04d304d2u, 0x04d304d3u, 0x04d604d6u, 0x04d904d7u, 0x04dc04d9u, + 0x04e004dfu, 0x04e004e0u, 0x04e104e0u, 0x04e204e1u, 0x04e404e4u, 0x04e404e4u, 0x04e704e7u, 0x04eb04e8u, + 0x04ef04edu, 0x04f304f2u, 0x04f704f4u, 0x04fb04f9u, 0x04fd04fdu, 0x05030500u, 0x05090505u, 0x050d050bu, + 0x05130510u, 0x05140514u, 0x05160515u, 0x05160516u, 0x051a0518u, 0x051d051bu, 0x051e051du, 0x0520051fu, + 0x05260523u, 0x05280527u, 0x05290529u, 0x052d052au, 0x0531052eu, 0x05320531u, 0x05350533u, 0x053a0538u, + 0x053d053bu, 0x053f053fu, 0x05420542u, 0x05420542u, 0x05440542u, 0x05480546u, 0x054c054bu, 0x054f054eu, + 0x054f054fu, 0x0551054fu, 0x05530552u, 0x05550553u, 0x05570557u, 0x055c0559u, 0x055e055du, 0x055e055eu, + 0x055f055fu, 0x0563055fu, 0x05660566u, 0x05690568u, 0x056c056bu, 0x056d056cu, 0x056e056du, 0x05710571u, + 0x05720572u, 0x05720572u, 0x05730573u, 0x05750573u, 0x05760575u, 0x05790578u, 0x057f057cu, 0x05830581u, + 0x05850585u, 0x05870587u, 0x058e0589u, 0x058f058eu, 0x05930592u, 0x05980595u, 0x059c059au, 0x05a205a0u, + 0x05a205a2u, 0x05a305a3u, 0x05a805a6u, 0x05aa05a9u, 0x05ac05acu, 0x05ad05adu, 0x05b005adu, 0x05b705b2u, + 0x05ba05b9u, 0x05bb05bbu, 0x05bd05bbu, 0x05bd05bdu, 0x05bf05bfu, 0x05c305c1u, 0x05c605c4u, 0x05c605c6u, + 0x05c705c7u, 0x05c905c8u, 0x05ca05c9u, 0x05cd05ccu, 0x05cf05cdu, 0x05d105d0u, 0x05d305d2u, 0x05d605d4u, + 0x05d805d6u, 0x05d905d8u, 0x05dc05dau, 0x05dc05dcu, 0x05de05dcu, 0x05df05deu, 0x05e005e0u, 0x05e405e3u, + 0x05e705e5u, 0x05ec05eau, 0x05ee05edu, 0x05f205f1u, 0x05f305f2u, 0x05f705f4u, 0x05f905f8u, 0x05fb05fbu, + 0x05fd05fcu, 0x06000600u, 0x06050604u, 0x06050605u, 0x06090609u, 0x060c060bu, 0x0610060fu, 0x06100610u, + 0x06130610u, 0x06140613u, 0x061a0618u, 0x061b061au, 0x061d061cu, 0x06250622u, 0x06260625u, 0x06280628u, + 0x062a0629u, 0x062b062bu, 0x062e062cu, 0x062e062eu, 0x06300630u, 0x06330632u, 0x06350635u, 0x06370636u, + 0x06390638u, 0x063a063au, 0x063b063au, 0x063d063cu, 0x0641063eu, 0x06420641u, 0x06450643u, 0x06480646u, + 0x064e064au, 0x064f064fu, 0x06510651u, 0x06530652u, 0x06570656u, 0x06580658u, 0x065a0659u, 0x065d065cu, + 0x065e065eu, 0x0661065fu, 0x06630662u, 0x06640664u, 0x06660666u, 0x06670666u, 0x06690668u, 0x066b0669u, + 0x066d066du, 0x066e066du, 0x06710670u, 0x06720671u, 0x06740673u, 0x06760676u, 0x06780678u, 0x067a0679u, + 0x067c067cu, 0x067e067eu, 0x067f067eu, 0x06820680u, 0x06860685u, 0x068a0688u, 0x068c068cu, 0x068e068cu, + 0x06990690u, 0x069b069au, 0x069b069bu, 0x069e069cu, 0x06a206a0u, 0x06a506a4u, 0x06a606a5u, 0x06ab06aau, + 0x06ad06adu, 0x06b006afu, 0x06b406b1u, 0x06b406b4u, 0x06b606b5u, 0x06ba06b8u, 0x06ba06bau, 0x06be06bdu, + 0x06c106c1u, 0x06c506c2u, 0x06c806c6u, 0x06c906c9u, 0x06ce06ccu, 0x06ce06ceu, 0x06d306d3u, 0x06d706d4u, + 0x06dd06dcu, 0x06dd06ddu, 0x06de06ddu, 0x06e006deu, 0x06e406e1u, 0x06e506e5u, 0x06e806e8u, 0x06e906e8u, + 0x06ed06ebu, 0x06ee06eeu, 0x06f106eeu, 0x06f206f2u, 0x06f606f3u, 0x06fa06f9u, 0x06fc06fbu, 0x06fd06fdu, + 0x07000700u, 0x07020700u, 0x07070703u, 0x070a0709u, 0x070c070cu, 0x070d070cu, 0x070d070du, 0x070f070eu, + 0x07100710u, 0x07120710u, 0x07120712u, 0x07140714u, 0x07150714u, 0x07160715u, 0x07170717u, 0x071a0719u, + 0x0720071eu, 0x07230720u, 0x07260725u, 0x07290727u, 0x072a0729u, 0x072d072bu, 0x072f072eu, 0x0732072fu, + 0x07340733u, 0x07370735u, 0x07370737u, 0x07380737u, 0x073a0739u, 0x073b073au, 0x073e073eu, 0x07410740u, + 0x07420741u, 0x07440743u, 0x07450744u, 0x07460746u, 0x07480747u, 0x074b074au, 0x074b074bu, 0x074e074du, + 0x0751074eu, 0x07520751u, 0x07550755u, 0x07580757u, 0x07590759u, 0x075c075bu, 0x075f075fu, 0x0760075fu, + 0x07610760u, 0x07660766u, 0x07690769u, 0x076a0769u, 0x076d076bu, 0x076e076eu, 0x0771076fu, 0x07740772u, + 0x07780774u, 0x077a0778u, 0x077d077cu, 0x077e077du, 0x0781077fu, 0x07830782u, 0x07830783u, 0x07840783u, + 0x07870786u, 0x07890788u, 0x078a0789u, 0x078c078bu, 0x078d078du, 0x078d078du, 0x078e078eu, 0x07920792u, + 0x07930792u, 0x07970796u, 0x07990799u, 0x079a0799u, 0x079d079au, 0x079e079eu, 0x07a007a0u, 0x07a007a0u, + 0x07a207a0u, 0x07a407a3u, 0x07a507a5u, 0x07a507a5u, 0x07a807a6u, 0x07ac07abu, 0x07ad07adu, 0x07ad07adu, + 0x07b207aeu, 0x07b407b3u, 0x07b607b6u, 0x07b907b9u, 0x07bd07bcu, 0x07be07bdu, 0x07bf07beu, 0x07c207bfu, + 0x07c407c4u, 0x07c507c5u, 0x07c507c5u, 0x07c607c5u, 0x07c707c6u, 0x07c907c8u, 0x07ca07cau, 0x07cc07cau, + 0x07d007ceu, 0x07d307d1u, 0x07d407d4u, 0x07d607d6u, 0x07d607d6u, 0x07d907d8u, 0x07db07dbu, 0x07dc07dbu, + 0x07de07ddu, 0x07df07dfu, 0x07e207e0u, 0x07e807e5u, 0x07eb07e9u, 0x07eb07ebu, 0x07ee07eeu, 0x07ef07efu, + 0x07f407f1u, 0x07f407f4u, 0x07f807f7u, 0x07f807f8u, 0x07f807f8u, 0x07fc07fau, 0x07fe07fcu, 0x080207ffu, + 0x08040803u, 0x08060806u, 0x08080806u, 0x080a0809u, 0x080a080au, 0x080d080bu, 0x08120810u, 0x08130813u, + 0x08160814u, 0x08180818u, 0x081b0819u, 0x081e081eu, 0x0821081eu, 0x08220821u, 0x08240824u, 0x08270826u, + 0x08270827u, 0x082b0829u, 0x082e082cu, 0x082f082eu, 0x0830082fu, 0x08310830u, 0x08320832u, 0x08340833u, + 0x08360834u, 0x08360836u, 0x08380837u, 0x08390839u, 0x083d083bu, 0x083f083du, 0x08410840u, 0x08410841u, + 0x08420841u, 0x08440843u, 0x08450845u, 0x08470846u, 0x084a0849u, 0x084d084bu, 0x084e084eu, 0x084f084fu, + 0x08530852u, 0x08540854u, 0x08550855u, 0x08570855u, 0x085a0858u, 0x085b085au, 0x085c085cu, 0x085d085cu, + 0x085d085du, 0x08630861u, 0x08650864u, 0x08670865u, 0x086a0867u, 0x086e086cu, 0x08710871u, 0x08730872u, + 0x08760873u, 0x08790878u, 0x08790879u, 0x087b087au, 0x087c087bu, 0x087c087cu, 0x087d087du, 0x0880087eu, + 0x08810881u, 0x08830883u, 0x08870886u, 0x08890888u, 0x088d088cu, 0x088e088du, 0x088f088fu, 0x08900890u, + 0x08920890u, 0x08960894u, 0x08970896u, 0x08970897u, 0x08990898u, 0x089d089cu, 0x089f089du, 0x08a208a2u, + 0x08a408a4u, 0x08a808a7u, 0x08a908a8u, 0x08ab08aau, 0x08ae08acu, 0x08af08aeu, 0x08b108b0u, 0x08b208b2u, + 0x08b408b2u, 0x08b608b4u, 0x08b808b7u, 0x08bb08b9u, 0x08bc08bcu, 0x08bd08bcu, 0x08bd08bdu, 0x08be08beu, + 0x08c008bfu, 0x08c108c0u, 0x08c508c3u, 0x08ca08c8u, 0x08cd08ccu, 0x08ce08cdu, 0x08ce08ceu, 0x08d108d0u, + 0x08d208d2u, 0x08d408d4u, 0x08d408d4u, 0x08d508d5u, 0x08d808d6u, 0x08dc08d9u, 0x08dc08dcu, 0x08dd08ddu, + 0x08e008dfu, 0x08e408e2u, 0x08e408e4u, 0x08e508e4u, 0x08e708e6u, 0x08e808e7u, 0x08eb08e9u, 0x08f008eeu, + 0x08f308f2u, 0x08f608f4u, 0x08f608f6u, 0x08f908f9u, 0x08fc08f9u, 0x08fe08fcu, 0x09010901u, 0x09020902u, + 0x09040902u, 0x09050904u, 0x09060906u, 0x09080907u, 0x090b090au, 0x090d090du, 0x090f090eu, 0x09100910u, + 0x09140913u, 0x09160915u, 0x09190917u, 0x091d091cu, 0x091f091eu, 0x09220920u, 0x09260926u, 0x092a0929u, + 0x092d092bu, 0x092e092du, 0x09320931u, 0x09340933u, 0x09350935u, 0x093b0938u, 0x093e093du, 0x0943093fu, + 0x09450945u, 0x09470946u, 0x09480947u, 0x094d0949u, 0x094f094fu, 0x09500950u, 0x09550951u, 0x09560955u, + 0x09560956u, 0x09570957u, 0x09590957u, 0x095c095au, 0x095c095cu, 0x095e095eu, 0x09600960u, 0x09610960u, + 0x09630962u, 0x09650964u, 0x09670967u, 0x096b096au, 0x096e096du, 0x09720970u, 0x09740974u, 0x09740974u, + 0x09750975u, 0x09790978u, 0x097d097bu, 0x097f097fu, 0x097f097fu, 0x09810981u, 0x09820981u, 0x09850983u, + 0x098b0985u, 0x098b098bu, 0x0990098du, 0x09920990u, 0x09940993u, 0x09950994u, 0x09990998u, 0x099e099cu, + 0x099f099eu, 0x09a209a1u, 0x09a309a3u, 0x09a509a4u, 0x09a909a7u, 0x09ad09aau, 0x09b009adu, 0x09b209b0u, + 0x09b209b2u, 0x09b409b2u, 0x09b509b4u, 0x09b509b5u, 0x09b609b5u, 0x09b609b6u, 0x09b909b8u, 0x09bb09bbu, + 0x09be09bdu, 0x09bf09beu, 0x09c109c0u, 0x09c109c1u, 0x09c209c1u, 0x09c409c3u, 0x09c709c6u, 0x09c909c9u, + 0x09ce09cbu, 0x09ce09ceu, 0x09cf09ceu, 0x09cf09cfu, 0x09d109d0u, 0x09d509d2u, 0x09d909d8u, 0x09d909d9u, + 0x09de09ddu, 0x09e009dfu, 0x09e209e1u, 0x09e609e3u, 0x09e809e8u, 0x09e809e8u, 0x09ec09e9u, 0x09ef09eeu, + 0x09f109f1u, 0x09f209f2u, 0x09f409f3u, 0x09f509f4u, 0x09f609f5u, 0x09f909f6u, 0x09fa09f9u, 0x09fd09fbu, + 0x09fe09feu, 0x09ff09ffu, 0x0a030a02u, 0x0a060a05u, 0x0a090a08u, 0x0a0c0a0bu, 0x0a120a0eu, 0x0a140a13u, + 0x0a170a16u, 0x0a190a18u, 0x0a1b0a1au, 0x0a1c0a1cu, 0x0a1d0a1du, 0x0a200a1fu, 0x0a220a20u, 0x0a250a24u, + 0x0a280a27u, 0x0a2b0a2au, 0x0a2d0a2cu, 0x0a300a2du, 0x0a320a31u, 0x0a360a35u, 0x0a380a36u, 0x0a3c0a3au, + 0x0a3d0a3cu, 0x0a3d0a3du, 0x0a3d0a3du, 0x0a3e0a3du, 0x0a400a3fu, 0x0a400a40u, 0x0a410a41u, 0x0a420a41u, + 0x0a420a42u, 0x0a440a43u, 0x0a440a44u, 0x0a440a44u, 0x0a470a47u, 0x0a490a47u, 0x0a490a49u, 0x0a4b0a4au, + 0x0a4e0a4cu, 0x0a500a4eu, 0x0a530a52u, 0x0a540a54u, 0x0a560a55u, 0x0a570a56u, 0x0a580a58u, 0x0a590a58u, + 0x0a590a59u, 0x0a5d0a5cu, 0x0a5e0a5du, 0x0a5f0a5fu, 0x0a600a5fu, 0x0a640a62u, 0x0a660a64u, 0x0a670a66u, + 0x0a6a0a68u, 0x0a6b0a6bu, 0x0a6e0a6eu, 0x0a700a6fu, 0x0a710a71u, 0x0a720a71u, 0x0a730a73u, 0x0a740a74u, + 0x0a750a75u, 0x0a760a76u, 0x0a780a77u, 0x0a7b0a79u, 0x0a7c0a7bu, 0x0a7e0a7cu, 0x0a7e0a7eu, 0x0a7f0a7eu, + 0x0a820a7fu, 0x0a840a83u, 0x0a860a85u, 0x0a880a86u, 0x0a8a0a89u, 0x0a8c0a8bu, 0x0a8f0a8du, 0x0a910a90u, + 0x0a910a91u, 0x0a950a92u, 0x0a970a96u, 0x0a990a98u, 0x0a9b0a99u, 0x0a9d0a9du, 0x0a9f0a9du, 0x0aa00aa0u, + 0x0aa40aa0u, 0x0aa50aa4u, 0x0aaa0aa6u, 0x0aab0aaau, 0x0aaf0aacu, 0x0ab00aafu, 0x0ab10ab1u, 0x0ab40ab1u, + 0x0ab50ab4u, 0x0aba0ab9u, 0x0abd0abbu, 0x0abf0abeu, 0x0ac20ac0u, 0x0ac50ac3u, 0x0ac50ac5u, 0x0ac80ac8u, + 0x00000ac9u, +}; + +constexpr std::array ReciprocalCorrections{ + 0x09230922u, 0x0dcd0dccu, 0x01d101d0u, 0x04e704e6u, 0x0e8d0e8cu, 0x05df05deu, 0x07a507a4u, 0x085b085au, + 0x0f7f0f7eu, 0x0e070e06u, 0x01a901a8u, 0x0bdf0bdeu, 0x06e306e2u, 0x04db04dau, 0x02650264u, 0x0a430a42u, + 0x0fda0e3au, 0x039b0fdbu, 0x03df03deu, 0x0ca30ca2u, 0x096e0c74u, 0x0df5096fu, 0x0e7c0b2au, 0x09020e7du, + 0x08c409abu, 0x005b005au, 0x096e072eu, 0x0e7d096fu, 0x04ff07a9u, 0x07530752u, 0x0a830a82u, 0x04670466u, + 0x087f087eu, 0x06ef0524u, 0x02880cb3u, 0x05b70289u, 0x0d2b0d2au, 0x05d5013eu, 0x08fb0ef3u, 0x03020a8du, + 0x0906042eu, 0x089f00d9u, 0x0f340ea5u, 0x03790f43u, 0x045b045au, 0x01b60f1fu, 0x02970938u, 0x062d0626u, + 0x02b408cfu, 0x0f670c20u, 0x02b50068u, 0x0cdd0cdcu, 0x06e10ebeu, 0x00ab0c9eu, 0x08a7076du, 0x01230da0u, + 0x02590258u, 0x056e051eu, 0x0ecb08ffu, 0x02ba06dcu, 0x03ac0396u, 0x03c303adu, 0x03720927u, 0x01250031u, + 0x12f6018fu, 0x058e0035u, 0x001101e0u, 0x0a6f06a0u, 0x03c2017eu, 0x0975055fu, 0x06490f45u, 0x09520742u, + 0x074d051fu, 0x087204f7u, 0x0c5a0a55u, 0x01650c5bu, 0x01f30dabu, 0x0d5b0364u, 0x087f0368u, 0x0e730953u, + 0x036101bcu, 0x064a1e36u, 0x03a602d2u, 0x0d240954u, 0x081e002au, 0x06690e87u, 0x035b0e6bu, 0x0aa9058au, + 0x044a0018u, 0x06f504f5u, 0x01160bbbu, 0x068101bfu, 0x02790121u, 0x033b0e62u, 0x0dc80b27u, 0x046b014du, + 0x00a70cb1u, 0x03a10230u, 0x03320549u, 0x092708ddu, 0x00640c76u, 0x193b01acu, 0x02eb0998u, 0x0fa30e9du, + 0x0d680286u, 0x0ae40808u, 0x03870f6du, 0x09fa0451u, 0x0a960098u, 0x09ef0cbeu, 0x031702c2u, 0x017d0185u, + 0x02ed03c8u, 0x003e0453u, 0x068801c4u, 0x0f4d0d62u, 0x0b360fe3u, 0x0afb0293u, 0x064a0c79u, 0x042a0ed6u, + 0x014e0c5du, 0x03530535u, 0x0499039fu, 0x02460953u, 0x086f0303u, 0x07b50d76u, 0x0dc40928u, 0x024e0a5au, + 0x068304ecu, 0x07b2076au, 0x07750f6fu, 0x0d3c02a5u, 0x0c6c03bfu, 0x086a00c7u, 0x02b50270u, 0x00ad06cdu, + 0x05b00515u, 0x097b063cu, 0x04d00df2u, 0x09890e9cu, 0x0d830992u, 0x0f40009au, 0x0d86022bu, 0x0ee70e34u, + 0x09560897u, 0x0d240cf7u, 0x06650343u, 0x0488008fu, 0x06bf053cu, 0x0ba80c1cu, 0x0caf0c32u, 0x071f0d84u, + 0x0ded0b02u, 0x07c40558u, 0x00220b18u, 0x018b04cdu, 0x098d05e3u, 0x0b1e0fb9u, 0x01710dc4u, 0x030b0accu, + 0x0f4d0a1cu, 0x01b20fcbu, 0x0273021bu, 0x0b27092du, 0x0d640d14u, 0x08cb0258u, 0x06b10bb5u, 0x0ab506a2u, + 0x01281af9u, 0x0eb00d04u, 0x06b102d1u, 0x0bc70b39u, 0x096b0783u, 0x062b0d74u, 0x01500e6fu, 0x1a11033bu, + 0x09ad0f37u, 0x07150d85u, 0x0a480fabu, 0x081b098eu, 0x0c850be0u, 0x03fc04bfu, 0x0e160d48u, 0x0e130c66u, + 0x075c01f8u, 0x03580e3du, 0x00bd0877u, 0x064602b4u, 0x052b0697u, 0x00bd0c4du, 0x02270179u, 0x096e06bbu, + 0x011c0a9cu, 0x04f80a78u, 0x0fcb020bu, 0x17e301a8u, 0x0e9909d5u, 0x0cbf06abu, 0x072d019du, 0x07110d1eu, + 0x03e60785u, 0x0f5807b7u, 0x076b0114u, 0x009f08b3u, 0x048b0153u, 0x09ed0879u, 0x0eeb0a69u, 0x05c500e7u, + 0x06530d54u, 0x18010680u, 0x06b502c8u, 0x0a8209f2u, 0x062204f0u, 0x10440a5au, 0x042e0254u, 0x0ca908a8u, + 0x0073003au, 0x074103a5u, 0x025a1908u, 0x02a10ce1u, 0x03e503c9u, 0x06ab043cu, 0x097008a3u, 0x05c20d91u, + 0x03250b4cu, 0x0cc70a0fu, 0x071a006eu, 0x00710c41u, 0x0b5a00b4u, 0x00300e76u, 0x08fa0994u, 0x0dfa0973u, + 0x054104cfu, 0x0d5c0d47u, 0x0abf0257u, 0x021d0bc4u, 0x043c03c3u, 0x0e16087fu, 0x09b30eafu, 0x17000164u, + 0x07750726u, 0x02f208d3u, 0x03f306e3u, 0x06280379u, 0x0dce0ae4u, 0x02c400c3u, 0x09d6067cu, 0x0bf10552u, + 0x09eb00bau, 0x0f890af0u, 0x03640035u, 0x00520f97u, 0x05590223u, 0x0c16028du, 0x0d840c21u, 0x068c0130u, + 0x00360f87u, 0x0598018eu, 0x080f07c4u, 0x04110cc6u, 0x09e404eeu, 0x089802d2u, 0x0ed60c97u, 0x0cd10722u, + 0x0ecf0dbbu, 0x0f6008f8u, 0x0b650931u, 0x06dc0cc0u, 0x0a9e07ddu, 0x0ed90ca7u, 0x01150058u, 0x0d020745u, + 0x06e80cc6u, 0x086f072au, 0x013f0e21u, 0x0fc61bdeu, 0x06ff00cau, 0x061a0d5fu, 0x073b06cfu, 0x0f650a40u, + 0x0bc60234u, 0x00280bd0u, 0x0bcd00c3u, 0x006e0f98u, 0x057e04c6u, 0x01ae07d0u, 0x09d7043du, 0x08710d0eu, + 0x08220b46u, 0x0a3909c6u, 0x0c320ab3u, 0x02e70dd1u, 0x0d850f0bu, 0x027f0ddcu, 0x0599044cu, 0x07710a83u, + 0x1b610ae2u, 0x0cca0b68u, 0x001c06aau, 0x002f08a7u, 0x0b990a66u, 0x04a4043du, 0x09c402d4u, 0x0ba20ba9u, + 0x02680fe7u, 0x0b8908a8u, 0x09ce0491u, 0x01af0966u, 0x0c3e0797u, 0x02750f5cu, 0x02d70a0du, 0x0057064au, + 0x0126018au, 0x0ed30792u, 0x0a5504c3u, 0x0cef04b9u, 0x07d10de6u, 0x0f5d0a98u, 0x0f8a0d52u, 0x03c60aebu, + 0x0caf088fu, 0x06b406eau, 0x09040a91u, 0x0bbb0a57u, 0x08a6003au, 0x0f590de3u, 0x06020fb7u, 0x008e083du, + 0x01940124u, 0x07a101bbu, 0x0f1e0c68u, 0x092c0310u, 0x09b4096du, 0x0d120297u, 0x02b70e2du, 0x0c2e19ceu, + 0x118d00a9u, 0x0cf7067fu, 0x01790e25u, 0x0dd91421u, 0x0f040dfeu, 0x0b3c0133u, 0x033b013bu, 0x0ac2048eu, + 0x0d050bcdu, 0x0da40eddu, 0x0ae2046fu, 0x0d1602a4u, 0x01d20669u, 0x0f9b0f24u, 0x04cb1b07u, 0x0d2f06feu, + 0x0f4c0ad0u, 0x076e0fd4u, 0x039a0ea2u, 0x05b203a2u, 0x0cfe0fc6u, 0x00040000u, 0x0cee04a8u, 0x0a9313e9u, + 0x08e60433u, 0x03370317u, 0x0f12092cu, 0x0e8e06b0u, 0x07191400u, 0x02480745u, 0x1bc10a4fu, 0x0930051du, + 0x18de0e5fu, 0x069f051au, 0x0e8e0abbu, 0x09a10231u, 0x0fe10afbu, 0x02f70146u, 0x0a38020cu, 0x0b380995u, + 0x0b420727u, 0x091a0494u, 0x07361c40u, 0x0c320376u, 0x09b10949u, 0x0a270370u, 0x059302c5u, 0x0f4a06cbu, + 0x0e1e069eu, 0x0e0503f9u, 0x0df6017cu, 0x07cb0184u, 0x0c1d08bau, 0x0e3500b8u, 0x02730c99u, 0x0a3f06ebu, + 0x063d06e2u, 0x05ff086cu, 0x0a8208b7u, 0x085f0660u, 0x02dd0f51u, 0x06130576u, 0x061a0581u, 0x00380e54u, + 0x06b102dfu, 0x0752029eu, 0x1c810abbu, 0x0a4502dbu, 0x0ecb0d85u, 0x051a01a6u, 0x09ea0774u, 0x0e390cf0u, + 0x04c40019u, 0x055100cfu, 0x037206c8u, 0x049403b8u, 0x005a153du, 0x0243011du, 0x00ac007cu, 0x088c06f7u, + 0x0ce70b5cu, 0x0e7b0d84u, 0x040902a2u, 0x031a084bu, 0x002e064au, 0x0c9d0cd2u, 0x00c8036cu, 0x0f1a07a7u, + 0x07420150u, 0x0b850f5du, 0x0bb20072u, 0x0cae0dbau, 0x06891371u, 0x04f40adfu, 0x08360659u, 0x12810f3bu, + 0x019108fdu, 0x037e0c9du, 0x0f2f0b57u, 0x0cc405a8u, 0x064a07d5u, 0x08340f67u, 0x02410d1fu, 0x08c506cbu, + 0x0d3e0d7fu, 0x088a0198u, 0x0d3b0eb6u, 0x08340031u, 0x01591f7du, 0x03e20cdfu, 0x0cf30c50u, 0x00fe1ef7u, + 0x050f01c4u, 0x07ac0c36u, 0x0658057du, 0x02c80743u, 0x0d3e0d4cu, 0x06570264u, 0x01ee0e6eu, 0x0ab30a4bu, + 0x01160098u, 0x00e307e3u, 0x07da050bu, 0x0349013eu, 0x00d10568u, 0x023014d7u, 0x0efa02a0u, 0x0b9f0f6au, + 0x0ff50d0fu, 0x034e08b8u, 0x03f40a10u, 0x09d60958u, 0x09670336u, 0x02d70e9du, 0x08490548u, 0x073f0012u, + 0x007c0b8fu, 0x19120fc8u, 0x0ed008dcu, 0x0d890533u, 0x030802fbu, 0x0e410c8du, 0x1cb80224u, 0x0e7d08fcu, + 0x18c108beu, 0x06ef0e4bu, 0x0b0b0b85u, 0x02bb019cu, 0x09331414u, 0x01900342u, 0x063b1bf8u, 0x0d2a0d59u, + 0x0b7a090du, 0x087f0e27u, 0x06dc046du, 0x05380a6du, 0x09070314u, 0x0ce40ee0u, 0x0a8d09b7u, 0x0e680367u, + 0x0d920486u, 0x091b0288u, 0x0d010fb4u, 0x09d80eceu, 0x0ea6048au, 0x02da0b5du, 0x01570b88u, 0x016c078bu, + 0x074d037cu, 0x08be078du, 0x025f0b3fu, 0x05360c4cu, 0x09b706f9u, 0x04b00b46u, 0x0c1208ebu, 0x1e8a0a46u, + 0x00080f72u, 0x003a0021u, 0x13f402cfu, 0x0642053du, 0x00e700aau, 0x04b10ad4u, 0x0fa00e90u, 0x00970eb0u, + 0x04740b05u, 0x16710ba9u, 0x0a3007d6u, 0x0b8a0a4fu, 0x0d2d0af9u, 0x02e109bbu, 0x0c650d75u, 0x0f5c01d9u, + 0x01990f6fu, 0x0ea30ac5u, 0x0d1507b3u, 0x0f7a0ed1u, 0x05ec0455u, 0x01b609e8u, 0x04bd0cc1u, 0x07bd0bd0u, + 0x02470b13u, 0x062a19afu, 0x04da0a7cu, 0x0ee308cau, 0x0be5000fu, 0x046d0ef7u, 0x050d0eccu, 0x04780947u, + 0x0b141721u, 0x04a30c26u, 0x00f30e27u, 0x086a0034u, 0x0efb0e58u, 0x09470553u, 0x0f770bf9u, 0x0c100059u, + 0x077b02e8u, 0x0e8507d0u, 0x0e3f075fu, 0x073601c9u, 0x05640dd8u, 0x075f0a4bu, 0x0ee60e3du, 0x057a0212u, + 0x0db10c5au, 0x067f0008u, 0x039301bbu, 0x085e0565u, 0x0efa0bc3u, 0x03380f6fu, 0x0cba0034u, 0x14d7100fu, + 0x096d006eu, 0x03d40d1au, 0x049609eeu, 0x0f7f0b78u, 0x0a0e08feu, 0x01e20b90u, 0x0e6705b0u, 0x0d3310c3u, + 0x055f0302u, 0x096b08c2u, 0x1acc09c1u, 0x08aa0345u, 0x037602c2u, 0x057504d4u, 0x0f3b0ee8u, 0x0aa00f66u, + 0x04fc0215u, 0x0e43005eu, 0x0ba60350u, 0x04c81cf9u, 0x0df707afu, 0x069707cbu, 0x0ed70965u, 0x007b05bcu, + 0x0c960f1au, 0x05f60c04u, 0x0e800dcbu, 0x09c70f82u, 0x04b40612u, 0x04e30c57u, 0x09c508b9u, 0x045b1f4au, + 0x0aa814d7u, 0x0c2b0ecbu, 0x180a1ef5u, 0x0ab0045du, 0x039100b3u, 0x0c7c0587u, 0x0d5b02a9u, 0x002d000eu, + 0x09720c68u, 0x0ecf0231u, 0x03c70afau, 0x0d6c06f5u, 0x01320bcfu, 0x04e811cfu, 0x0cdf0f7bu, 0x06ca09deu, + 0x0b0e06c8u, 0x01230562u, 0x0fb20b65u, 0x02c70882u, 0x08c40749u, 0x0d480b66u, 0x177103cfu, 0x06ca09f2u, + 0x063c0456u, 0x0add00d5u, 0x0b3308afu, 0x0ea30f84u, 0x0b7204d7u, 0x04e70e35u, 0x043b0a95u, 0x18450cbfu, + 0x0fd70a2bu, 0x0c8c03e2u, 0x087a008au, 0x1dd31d76u, 0x00300f35u, 0x0bd6084eu, 0x01050d8bu, 0x06600568u, + 0x0c9e0c4du, 0x13550f6au, 0x15bc073au, 0x082306ddu, 0x02500c6cu, 0x15710e9bu, 0x084f1eedu, 0x0a5f0949u, + 0x046a000eu, 0x092317f2u, 0x0cd11c78u, 0x05570228u, 0x061c008bu, 0x092d081du, 0x04b40b07u, 0x01b706ecu, + 0x099414f2u, 0x120d0a3cu, 0x0e600a1bu, 0x1719049cu, 0x08da0984u, 0x0627033eu, 0x011a09c4u, 0x0727010du, + 0x04f0082cu, 0x057a015du, 0x1a8509d7u, 0x0d2b1cccu, 0x097d0792u, 0x01c80e69u, 0x02bb0366u, 0x009b0c68u, + 0x0a2909f7u, 0x0e7d071au, 0x0e941c04u, 0x03a911d3u, 0x0a240957u, 0x0c940bf4u, 0x0b5f0b28u, 0x028e0fc5u, + 0x08ae0d84u, 0x05d20c71u, 0x05710aedu, 0x052e0a9cu, 0x054e0ac8u, 0x0f880c7eu, 0x0ef6083du, 0x006f0a28u, + 0x19d50787u, 0x02ac0f28u, 0x0a5f033cu, 0x1e5c0c9bu, 0x0d2f0553u, 0x0fe60f84u, 0x08ae0214u, 0x09d1016au, + 0x083d0d58u, 0x11e00e5du, 0x02ea0e50u, 0x0e27081du, 0x028002eau, 0x099205d2u, 0x0c5e0726u, 0x09ba1e66u, + 0x00290ef6u, 0x05b50223u, 0x079305efu, 0x06e31f47u, 0x00b00e11u, 0x02b0028fu, 0x09fe0634u, 0x024901d1u, + 0x07251586u, 0x0a681520u, 0x02571e25u, 0x0e8802c8u, 0x0f130029u, 0x1aa606ecu, 0x1c6015fau, 0x0f990ef3u, + 0x089406f0u, 0x02b30befu, 0x0f1b0d89u, 0x04a60145u, 0x10980c5bu, 0x06a2017fu, 0x167f1e12u, 0x0b0c19a0u, + 0x19a500ceu, 0x04f6018eu, 0x0e710c36u, 0x00120f15u, 0x00001e03u, 0x0156194fu, 0x14b80330u, 0x1e570bc2u, + 0x01050f3du, 0x1af905a8u, 0x09eb0862u, 0x0d5f05b1u, 0x048c03ebu, 0x0963061au, 0x19dc019du, 0x04691bebu, + 0x07f20e07u, 0x0fcf0bbbu, 0x1cc007fdu, 0x1e800b64u, 0x04651b38u, 0x0b9d0a7du, 0x0b720005u, 0x0fd6063du, + 0x06ae11bau, 0x013816b1u, 0x082c021bu, 0x03221beau, 0x16f906f8u, 0x025001a2u, 0x009f1a20u, 0x0acd100fu, + 0x0cbf0ef2u, 0x0569036du, 0x070e102bu, 0x06210b4du, 0x18fa0d3eu, 0x04fd0b06u, 0x084507b6u, 0x0bea1a45u, + 0x08be0573u, 0x0d0a1be9u, 0x192700a4u, 0x0ad60a34u, 0x05410f23u, 0x08c40c2du, 0x08bf026du, 0x007e09bbu, + 0x012c1041u, 0x0a5911b9u, 0x18a202c8u, 0x0b540a76u, 0x07af0530u, 0x040309e1u, 0x11f90406u, 0x0b5f032fu, + 0x0db00d5eu, 0x081b07deu, 0x0e291f5eu, 0x1ab202d6u, 0x0ab60b13u, 0x0b3107b4u, 0x069c11f8u, 0x09a3163eu, + 0x056b026cu, 0x042608b5u, 0x01c30788u, 0x079414e1u, 0x085f054fu, 0x02fd1d41u, 0x169e0794u, 0x0d5d1680u, + 0x074f1ffcu, 0x192e091au, 0x1b411e08u, 0x0b4f0a9au, 0x0e570b1eu, 0x0288001fu, 0x078e05a2u, 0x01b01c0du, + 0x069313e1u, 0x0ad708feu, 0x0ed01dcfu, 0x060b0a56u, 0x03c807e4u, 0x1fbc0d62u, 0x038c1d8du, 0x033609fcu, + 0x09ca086au, 0x0a260bacu, 0x0f7708e4u, 0x0774040fu, 0x0e340e69u, 0x0cfe0e3fu, 0x0e9f0fbfu, 0x0a4f0715u, + 0x01160fcdu, 0x075f0642u, 0x0a3f090du, 0x0dac0382u, 0x03b40ce9u, 0x16410523u, 0x0355003eu, 0x02590d33u, + 0x0c6516bdu, 0x088f05c3u, 0x068a0971u, 0x0f4b0dd7u, 0x07d9004fu, 0x086a1548u, 0x07341000u, 0x068c0274u, + 0x00090000u, 0x001b0012u, 0x002d0024u, 0x003f0036u, 0x03a006e2u, 0x0c7a0985u, 0x13a310f2u, 0x0b491025u, + 0x033800d3u, 0x1c7206e3u, 0x0c691176u, 0x1a11016bu, 0x0cc20b72u, 0x05710df1u, 0x15680f1au, 0x0cd108aau, + 0x04630ce6u, 0x0c35050eu, 0x046004afu, 0x0fed061du, 0x1be00967u, 0x0d1d16c4u, 0x0f640e55u, 0x07e00417u, + 0x14190c5au, 0x1d921554u, 0x1df90ea5u, 0x0eb00a5eu, 0x110309cau, 0x0e5a0df9u, 0x0e690a65u, 0x075801eeu, + 0x0db30a05u, 0x0ee30f7bu, 0x04cf02d1u, 0x008a094du, 0x08a51872u, 0x0e300bd0u, 0x09450f24u, 0x0fb7016cu, + 0x00960bcbu, 0x0ade00f8u, 0x10940bfcu, 0x075e1465u, 0x031a0539u, 0x00111b10u, 0x0b15187bu, 0x035200b4u, + 0x0a2e1bdeu, 0x112e195bu, 0x095a13eau, 0x09ce085eu, 0x18060f1du, 0x07130d9du, 0x182d0b88u, 0x1c990a9du, + 0x1ac702fdu, 0x01cc0bf4u, 0x04ea04d7u, 0x05870cd2u, 0x0afe0a48u, 0x0d421cb2u, 0x02d71310u, 0x0df21bf7u, + 0x0c0e165eu, 0x040a0165u, 0x015401adu, 0x11a0021bu, 0x0daa0d0eu, 0x07780545u, 0x0a3c098eu, 0x1f620f4bu, + 0x04da034du, 0x038f1e83u, 0x01ef0a87u, 0x014e0b47u, 0x19c6028du, 0x1264026bu, 0x075b0a7eu, 0x0480044cu, + 0x0c4a1acbu, 0x0024022fu, 0x11820dc5u, 0x08241f8fu, 0x07c00859u, 0x03f50058u, 0x0d1a0430u, 0x099306a9u, + 0x00d60f4du, 0x0eb308b5u, 0x10360779u, 0x0c6e0606u, 0x05e603b3u, 0x032a0b25u, 0x11c00ea9u, 0x1632033eu, + 0x0d40078bu, 0x022710a8u, 0x03530a0fu, 0x07f206edu, 0x02c209e0u, 0x0f9a0cd0u, 0x0e1f1817u, 0x099f0fc7u, + 0x00331fbeu, 0x05a814f9u, 0x08cb16f3u, 0x0b0004ccu, 0x07430d5du, 0x04330e7eu, 0x17e20c33u, 0x03ec1ddcu, + 0x03ea1604u, 0x0eac0c76u, 0x00210000u, 0x0d0d19dcu, 0x0ae615fcu, 0x1b300887u, 0x0a060fdbu, 0x1d4a08dau, + 0x0ef51ff9u, 0x0514076fu, 0x0258005fu, 0x0d641050u, 0x015b0045u, 0x08090032u, 0x0c250a8du, 0x07030422u, + 0x0d180a87u, 0x00860588u, 0x0e5608c4u, 0x11110c54u, 0x15110b74u, 0x096d0068u, 0x089c0b25u, 0x064102dau, + 0x15cd048au, 0x00f308f1u, 0x0ddb1fb3u, 0x0d8e06aau, 0x07360644u, 0x02910f00u, 0x076e16bfu, 0x00420000u, + 0x0f821945u, 0x1e1202c5u, 0x08170125u, 0x07d50672u, 0x03131806u, 0x1b5904beu, 0x0c5a03dau, 0x104908f8u, + 0x0f6e1de5u, 0x1c2016a6u, 0x01ec02b7u, 0x0bbc11f9u, 0x1df102d1u, 0x0e880345u, 0x06190350u, 0x1935015eu, + 0x0185006bu, 0x03640955u, 0x165711ceu, 0x0a3102fbu, 0x08d802fbu, 0x0c760ac8u, 0x07ed0103u, 0x02040b8eu, + 0x0ff50cdcu, 0x1e6707c3u, 0x0d351edfu, 0x1ea41e21u, 0x00450603u, 0x055c0b12u, 0x0fab1f82u, 0x08430a7eu, + 0x005419ddu, 0x076416beu, 0x08c3014au, 0x0b9d19a3u, 0x09a112b2u, 0x1b6906d8u, 0x0262011cu, 0x014715f9u, + 0x076d15e0u, 0x0aae0380u, 0x086d022eu, 0x1fe3003eu, 0x01071660u, 0x135d0e1du, 0x070a16c5u, 0x0c7c0ad7u, + 0x1f540663u, 0x061612b5u, 0x1ce40423u, 0x06561183u, 0x03b904ccu, 0x04470860u, 0x0c0e0a18u, 0x1a4f0997u, + 0x00670b7du, 0x0e9f113du, 0x0a601481u, 0x05560d5bu, 0x08cb0a58u, 0x1e190c37u, 0x164301fbu, 0x03370418u, + 0x04c20a9cu, 0x023918a4u, 0x079b0010u, 0x069f00a9u, 0x00a40146u, 0x141c0687u, 0x13d91a7eu, 0x10b81811u, + 0x0b62170au, 0x1bc10fbbu, 0x017d00a1u, 0x04a707dcu, 0x019c08d3u, 0x03dd1ea4u, 0x0c190f3au, 0x071b028eu, + 0x09990925u, 0x01570979u, 0x0529006fu, 0x03e118f1u, 0x1b7e01d2u, 0x14d609cbu, 0x11280c7eu, 0x01b90190u, + 0x006b09b0u, 0x0f0c0566u, 0x043a0d40u, 0x055c05e4u, 0x0f510979u, 0x17a50103u, 0x0f3c0688u, 0x126c1269u, + 0x05251cbfu, 0x000704d9u, 0x19910048u, 0x0fe21090u, 0x1ecb17b3u, 0x0f2e0c18u, 0x05460a35u, 0x07560314u, + 0x0d68054du, 0x13291e85u, 0x0d751c10u, 0x1d4815b4u, 0x041d08f3u, 0x0f8107b4u, 0x0b520641u, 0x02f20cefu, + 0x09e70742u, 0x19e004d9u, 0x0e2d0c5du, 0x00201b11u, 0x108e0118u, 0x03cf03ccu, 0x003b0b77u, 0x012a08d5u, + 0x0aca09a4u, 0x0ed80807u, 0x18171265u, 0x057f097fu, 0x043d01cau, 0x005d0c51u, 0x1efd079au, 0x0c03012fu, + 0x184e1258u, 0x00310000u, 0x1f960f23u, 0x11f00ff4u, 0x140f038du, 0x0a421607u, 0x01620388u, 0x0eee0d9bu, + 0x0cd80a39u, 0x0aea0d38u, 0x05b90ba7u, 0x0d250730u, 0x07830276u, 0x0c2915f9u, 0x0ba20be3u, 0x0aa60303u, + 0x0e861e0cu, 0x1fbb07aeu, 0x04bf0d1cu, 0x11b4005au, 0x10b11792u, 0x1d870202u, 0x023b0bfdu, 0x15b81f61u, + 0x0b3008bcu, 0x0cf01148u, 0x10800ec6u, 0x0fd8018bu, 0x00e314c5u, 0x005805d4u, 0x08961309u, 0x15a400cbu, + 0x1b481956u, 0x04770015u, 0x0d021957u, 0x16a713feu, 0x008b0876u, 0x1d5318c1u, 0x09a70279u, 0x08d703cau, + 0x05310a00u, 0x0dba1a6bu, 0x03970e3eu, 0x00301562u, 0x1cc214d6u, 0x168c0949u, 0x1a0206b1u, 0x0def0f0cu, + 0x02460055u, 0x03ca05c4u, 0x0b591ac3u, 0x188f128eu, 0x01980a22u, 0x156502a8u, 0x07cd100cu, 0x1a7004b2u, + 0x0151007eu, 0x1863023du, 0x0ac10a73u, 0x05df1469u, 0x146f0666u, 0x0f27043bu, 0x17161f46u, 0x0a7314a8u, + 0x05d90c56u, 0x0bb215d3u, 0x03ba0edeu, 0x08c716d7u, 0x14450138u, 0x02d01b31u, 0x0eea0cc4u, 0x016d0326u, + 0x058404c4u, 0x03c5066du, 0x066105deu, 0x05ef014cu, 0x056d05a8u, 0x17c0165cu, 0x02b109a3u, 0x0382092cu, + 0x13e707dcu, 0x0c8a143du, 0x05550d36u, 0x13b916adu, 0x0e9f0f47u, 0x0f3f137cu, 0x0c1010d1u, 0x04c80072u, + 0x006403e8u, 0x06a20a4au, 0x0a3e004au, 0x03720f4eu, 0x0eae0d91u, 0x05980076u, 0x1bf10a5au, 0x15191332u, + 0x1b980322u, 0x0bdc1ad4u, 0x121a1ef7u, 0x048f03feu, 0x17000b34u, 0x18fc0c20u, 0x04e61979u, 0x12241579u, + 0x074a14d7u, 0x05860361u, 0x000e0be1u, 0x0030001fu, 0x00520041u, 0x00f61e9bu, 0x00130615u, 0x0c7e0964u, + 0x045f00cdu, 0x14061a21u, 0x023d085bu, 0x093d06fbu, 0x046d0deau, 0x0a6407d6u, 0x194d1640u, 0x00a6085bu, + 0x013009bcu, 0x058116a0u, 0x09541decu, 0x03c01dd0u, 0x0d240864u, 0x05950108u, 0x16761db9u, 0x0ef606a8u, + 0x15fe05ebu, 0x0f7715e9u, 0x174d1f96u, 0x1e3504a6u, 0x0165004bu, 0x02130278u, 0x09a90a61u, 0x177d063cu, + 0x09070105u, 0x05ca1855u, 0x1a051152u, 0x07851330u, 0x1df209a7u, 0x00001f55u, 0x07440007u, 0x1a5d1cbfu, + 0x0cbc0885u, 0x089f00d2u, 0x02e91d3au, 0x0bf9092bu, 0x11551958u, 0x19b503cau, 0x06b40a1au, 0x18110ed0u, + 0x05bf07a6u, 0x03cf07fdu, 0x1d4008f3u, 0x0bf312c8u, 0x000a070au, 0x0e1a0abeu, 0x1e5e1dc8u, 0x128006c2u, + 0x0d861299u, 0x0ba303c8u, 0x07761fceu, 0x0b7409f7u, 0x09bd1efcu, 0x05ad184fu, 0x0aba1b89u, 0x00790713u, + 0x0a631655u, 0x09640f98u, 0x132f196du, 0x043e0351u, 0x13c707c7u, 0x08630605u, 0x0ee30212u, 0x0e6e0397u, + 0x0f0a1ebcu, 0x0fc41ec8u, 0x0c651a13u, 0x0ada1d6cu, 0x09571288u, 0x062a0916u, 0x087e14b4u, 0x0bd70e45u, + 0x172611e8u, 0x0e6815b9u, 0x12850c43u, 0x17db14a3u, 0x0f9b1a71u, 0x07021de6u, 0x08f01334u, 0x1dba1ccbu, + 0x07bd01d0u, 0x16440166u, 0x017a1ad3u, 0x1d210b61u, 0x160c1080u, 0x070c0405u, 0x0537192bu, 0x08d412acu, + 0x0b1d0786u, 0x0a560117u, 0x04a20130u, 0x04ca0b7bu, 0x082709bfu, 0x0da50a93u, 0x19590007u, 0x16ba068bu, + 0x1ed401c3u, 0x00b50f57u, 0x165c0a4fu, 0x0a680e76u, 0x010c1db6u, 0x0a7c07e9u, 0x12e818f3u, 0x1f991014u, + 0x155c02f6u, 0x0e7600f4u, 0x0747008eu, 0x1a6413d0u, 0x0d0303b6u, 0x12d105e2u, 0x04bc03e6u, 0x01741353u, + 0x12c1062du, 0x1a8c0f24u, 0x046606d9u, 0x11c106d6u, 0x18850fa7u, 0x0cce097au, 0x1f9b1509u, 0x000001eeu, + 0x003e001fu, 0x1b2c08b8u, 0x152b0237u, 0x1deb04ebu, 0x14001b2fu, 0x11280499u, 0x006b1be7u, 0x160015e1u, + 0x00f41df0u, 0x08181371u, 0x01530a86u, 0x0da20b03u, 0x0a820433u, 0x049c0ab5u, 0x0816059fu, 0x0d640e1fu, + 0x0cd00303u, 0x1f9b1d50u, 0x0f040c3cu, 0x19e017dbu, 0x0fb20b51u, 0x18fa0384u, 0x0d4f08e4u, 0x0a890da6u, + 0x07e7120fu, 0x1de00cf4u, 0x04e01320u, 0x0826070au, 0x1a740ff8u, 0x012a1619u, 0x1a2d0481u, 0x00741488u, + 0x069d0388u, 0x11f00f3cu, 0x03ff1615u, 0x1720071eu, 0x00001486u, +}; + +template +std::uint32_t DefineTable(SpirvEmitterState& state, const std::array& values) { + std::array words{}; + for (std::size_t index = 0; index < Size; ++index) words[index] = ConstantU32(state, values[index]); + const auto arrayType = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, static_cast(Size))); + const auto table = state.module.Constant(spv::OpConstantComposite, arrayType, std::span(words)); + return state.module.DefineInitializedGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate, table); +} + +std::uint32_t LoadTable(SpirvEmitterState& state, std::uint32_t table, std::uint32_t index) { + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassPrivate, TypeU32(state)), pointer, table, index); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), value, pointer); + return value; +} + +std::array MultiplyExtended(SpirvEmitterState& state, std::uint32_t lhs, std::uint32_t rhs) { + const auto product = state.module.AllocateId(); + state.module.AddFunction(spv::OpUMulExtended, TypeU32Pair(state), product, lhs, rhs); + std::array words{state.module.AllocateId(), state.module.AllocateId()}; + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), words[0], product, 0u); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), words[1], product, 1u); + return words; +} + +std::uint32_t ReciprocalCorrection(SpirvEmitterState& state, std::uint32_t mantissa) { + const auto u32 = TypeU32(state); + const auto boolean = TypeBool(state); + const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, u32, lhs, ConstantU32(state, value)); }; + const auto starts = DefineTable(state, ReciprocalCorrectionStarts); + const auto corrections = DefineTable(state, ReciprocalCorrections); + const auto halfword = [&](std::uint32_t table, std::uint32_t index, std::uint32_t last) { + const auto clamped = Select(state, u32, Binary(state, spv::OpULessThan, boolean, index, ConstantU32(state, last)), index, ConstantU32(state, last)); + const auto word = LoadTable(state, table, op(spv::OpShiftRightLogical, clamped, 1u)); + return op(spv::OpBitwiseAnd, Binary(state, spv::OpShiftRightLogical, u32, word, op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, clamped, 1u), 4u)), 0xffffu); + }; + const auto lastStart = static_cast(ReciprocalCorrectionStarts.size() * 2u - 1u); + const auto lastCorrection = static_cast(ReciprocalCorrections.size() * 2u - 1u); + const auto bucket = op(spv::OpShiftRightLogical, mantissa, 12u); + const auto key = op(spv::OpBitwiseAnd, mantissa, 0xfffu); + const auto end = halfword(starts, op(spv::OpIAdd, bucket, 1u), lastStart); + auto first = halfword(starts, bucket, lastStart); + auto remaining = Binary(state, spv::OpISub, u32, end, first); + for (const std::uint32_t step : {8u, 4u, 2u, 1u}) { + const auto probe = op(spv::OpBitwiseAnd, halfword(corrections, op(spv::OpIAdd, first, step - 1u), lastCorrection), 0xfffu); + const auto inside = Binary(state, spv::OpULessThanEqual, boolean, ConstantU32(state, step), remaining); + const auto skip = Binary(state, spv::OpLogicalAnd, boolean, inside, Binary(state, spv::OpULessThan, boolean, probe, key)); + first = Select(state, u32, skip, op(spv::OpIAdd, first, step), first); + remaining = Select(state, u32, skip, op(spv::OpISub, remaining, step), remaining); + } + const auto entry = halfword(corrections, first, lastCorrection); + const auto hit = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpULessThan, boolean, first, end), Binary(state, spv::OpIEqual, boolean, op(spv::OpBitwiseAnd, entry, 0xfffu), key)); + const auto delta = Select(state, u32, Binary(state, spv::OpINotEqual, boolean, op(spv::OpBitwiseAnd, entry, 0x1000u), ConstantU32(state, 0u)), ConstantU32(state, 1u), ConstantU32(state, 0xffffffffu)); + return Select(state, u32, hit, delta, ConstantU32(state, 0u)); +} + +bool ExactReciprocal() { + static const bool exact = std::getenv("APS5_EXACT_RCP") != nullptr; + return exact; +} + +std::uint32_t DeviceReciprocal(SpirvEmitterState& state, std::uint32_t value) { + return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), value); +} + +std::uint32_t HardwareReciprocal(SpirvEmitterState& state, std::uint32_t value) { + const auto u32 = TypeU32(state); + const auto f32 = TypeF32(state); + const auto boolean = TypeBool(state); + const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t constant) { return Binary(state, opcode, u32, lhs, ConstantU32(state, constant)); }; + const auto add = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpIAdd, u32, lhs, rhs); }; + const auto sub = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpISub, u32, lhs, rhs); }; + const auto test = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, opcode, boolean, lhs, rhs); }; + const auto pick = [&](std::uint32_t condition, std::uint32_t whenTrue, std::uint32_t whenFalse) { return Select(state, u32, condition, ConstantU32(state, whenTrue), ConstantU32(state, whenFalse)); }; + if (state.reciprocalTables[0] == 0u) { + state.reciprocalTables = {DefineTable(state, ReciprocalConstant), DefineTable(state, ReciprocalLinear), DefineTable(state, ReciprocalHigherOrder)}; + } + const auto bits = Unary(state, spv::OpBitcast, u32, value); + const auto exponent = op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, bits, 23u), 0xffu); + const auto mantissa = op(spv::OpBitwiseAnd, bits, 0x7fffffu); + const auto segment = op(spv::OpShiftRightLogical, mantissa, 18u); + const auto fraction = op(spv::OpBitwiseAnd, mantissa, 0x3ffffu); + const auto constant = LoadTable(state, state.reciprocalTables[0], segment); + const auto linearFactor = LoadTable(state, state.reciprocalTables[1], segment); + const auto higherOrder = LoadTable(state, state.reciprocalTables[2], segment); + + const auto linearProduct = MultiplyExtended(state, linearFactor, fraction); + const auto wide = test(spv::OpUGreaterThanEqual, linearProduct[1], ConstantU32(state, 64u)); + const auto shift = pick(wide, 15u, 14u); + const auto quotient = Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpShiftLeftLogical, u32, linearProduct[1], sub(ConstantU32(state, 32u), shift)), Binary(state, spv::OpShiftRightLogical, u32, linearProduct[0], shift)); + const auto midpoint = pick(wide, 0x4000u, 0x2000u); + const auto remainder = Binary(state, spv::OpBitwiseAnd, u32, linearProduct[0], pick(wide, 0x7fffu, 0x3fffu)); + const auto odd = test(spv::OpINotEqual, op(spv::OpBitwiseAnd, quotient, 1u), ConstantU32(state, 0u)); + const auto roundUp = Binary(state, spv::OpLogicalOr, boolean, test(spv::OpUGreaterThan, remainder, midpoint), Binary(state, spv::OpLogicalAnd, boolean, test(spv::OpIEqual, remainder, midpoint), odd)); + const auto rounded = add(quotient, pick(roundUp, 1u, 0u)); + const auto linear = Binary(state, spv::OpShiftLeftLogical, u32, rounded, pick(wide, 1u, 0u)); + + const auto half = op(spv::OpShiftRightLogical, fraction, 1u); + const auto quadraticProduct = MultiplyExtended(state, op(spv::OpBitwiseAnd, higherOrder, 0xffffu), half); + const auto shiftedHigh = Binary(state, spv::OpBitwiseOr, u32, op(spv::OpShiftLeftLogical, quadraticProduct[1], 18u), op(spv::OpShiftRightLogical, quadraticProduct[0], 14u)); + const auto shiftedLow = op(spv::OpShiftLeftLogical, quadraticProduct[0], 18u); + const auto cubicProduct = MultiplyExtended(state, Binary(state, spv::OpIMul, u32, op(spv::OpShiftRightLogical, higherOrder, 16u), fraction), half); + const auto innerLow = sub(shiftedLow, cubicProduct[0]); + const auto innerHigh = sub(sub(shiftedHigh, cubicProduct[1]), pick(test(spv::OpULessThan, shiftedLow, cubicProduct[0]), 1u, 0u)); + const auto upper = MultiplyExtended(state, innerHigh, half); + const auto lower = MultiplyExtended(state, innerLow, half); + const auto upperPart = Binary(state, spv::OpBitwiseOr, u32, op(spv::OpShiftLeftLogical, upper[1], 15u), op(spv::OpShiftRightLogical, upper[0], 17u)); + const auto carryPart = op(spv::OpShiftRightLogical, add(op(spv::OpBitwiseAnd, upper[0], 0x1ffffu), lower[1]), 17u); + const auto quadratic = add(upperPart, carryPart); + + const auto sum = add(sub(op(spv::OpShiftLeftLogical, constant, 4u), linear), op(spv::OpIAdd, op(spv::OpShiftLeftLogical, quadratic, 1u), 32u)); + const auto dropped = op(spv::OpBitwiseAnd, sum, 63u); + const auto nearCut = Binary(state, spv::OpLogicalOr, boolean, test(spv::OpULessThan, dropped, ConstantU32(state, 2u)), test(spv::OpUGreaterThanEqual, dropped, ConstantU32(state, 62u))); + const auto correction = EmitValueOrZeroIfCondition(state, nearCut, [&] { + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, u32, result, state.reciprocalCorrectionFunction, mantissa); + return result; + }); + const auto normalized = add(op(spv::OpIAdd, op(spv::OpShiftRightLogical, sum, 6u), 0x3e800000u), correction); + const auto resultExponent = sub(op(spv::OpIAdd, op(spv::OpShiftRightLogical, normalized, 23u), 127u), exponent); + const auto exactBits = Binary(state, spv::OpBitwiseOr, u32, Binary(state, spv::OpBitwiseOr, u32, op(spv::OpBitwiseAnd, bits, 0x80000000u), op(spv::OpShiftLeftLogical, resultExponent, 23u)), op(spv::OpBitwiseAnd, normalized, 0x7fffffu)); + const auto normalInput = Binary(state, spv::OpLogicalAnd, boolean, test(spv::OpINotEqual, exponent, ConstantU32(state, 0u)), test(spv::OpINotEqual, exponent, ConstantU32(state, 0xffu))); + const auto normalResult = Binary(state, spv::OpLogicalAnd, boolean, test(spv::OpSGreaterThan, resultExponent, ConstantU32(state, 0u)), test(spv::OpSLessThan, resultExponent, ConstantU32(state, 0xffu))); + const auto approximate = DeviceReciprocal(state, value); + return Select(state, f32, Binary(state, spv::OpLogicalAnd, boolean, normalInput, normalResult), Unary(state, spv::OpBitcast, f32, exactBits), approximate); +} + +} + +void DefineReciprocalCorrectionFunction(SpirvEmitterState& state) { + bool used = false; + for (const IrBlock* block : state.program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) used = used || inst->Opcode() == IrOpcode::FPRecip32 || inst->Opcode() == IrOpcode::FPRecipIFlag32; + } + if (!used || !ExactReciprocal()) return; + const auto u32 = TypeU32(state); + state.reciprocalCorrectionFunction = state.module.AllocateId(); + state.module.AddName(state.reciprocalCorrectionFunction, "rcp_f32_correction"); + state.module.AddFunction(spv::OpFunction, u32, state.reciprocalCorrectionFunction, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, u32, u32)); + const auto mantissa = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u32, mantissa); + EmitLabel(state, state.module.AllocateId()); + state.module.AddFunction(spv::OpReturnValue, ReciprocalCorrection(state, mantissa)); + state.module.AddFunction(spv::OpFunctionEnd); +} + +std::uint32_t EmitFPRecip32(SpirvEmitterState& state, std::uint32_t arg0) { + if (!ExactReciprocal()) return DeviceReciprocal(state, arg0); + if (state.reciprocalCorrectionFunction == 0u) FailEmit("v_rcp_f32 correction function was not defined before function emission"); + return HardwareReciprocal(state, arg0); +} + +std::uint32_t EmitFPRecipIFlag32(SpirvEmitterState& state, std::uint32_t arg0) { + if (!ExactReciprocal()) return DeviceReciprocal(state, arg0); + if (state.reciprocalCorrectionFunction == 0u) FailEmit("v_rcp_iflag_f32 correction function was not defined before function emission"); + return HardwareReciprocal(state, arg0); +} + +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index ed2edb7466..2c9ebf55bd 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1534,6 +1534,7 @@ void EmitProgram(SpirvEmitterState& state) { } DefineGetBdaPointer(state); DefineDot2F32F16Function(state); + DefineReciprocalCorrectionFunction(state); for (const IrBlock* block : program.BlockOrder()) { const bool needsScratch = std::any_of(block->Instructions().begin(), block->Instructions().end(), [](const IrValue* inst) { return inst->Opcode() == IrOpcode::SwizzleU32; diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d14be69b73..9df29ac79b 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -28,6 +28,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_BDA_BYTE_READS` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp` | | forces one address lookup and byte load per byte for BDA reads instead of a wide dword load | | `APS5_BDA_DWORD_STORES` | presence | `core/shader/recompiler/Translation/src/MemoryInstructions/FlatInstructions.cpp` | | reverts BDA writes to one address store per dword instead of a wide multi-dword store | | `APS5_DUMP_IR` | string | `core/shader/recompiler/Recompiler.cpp` | | hex code address (or all) of the programs whose IR is printed after each front-end pass | +| `APS5_EXACT_RCP` | presence | `core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp` | | emits `v_rcp_f32` and `v_rcp_iflag_f32` as the RDNA hardware reciprocal (bit-exact for normal inputs and results) instead of the driver's division; adds about 13k SPIR-V words to each shader that uses them | | `APS5_GATHER_CONST` | integer | `core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp` | | integer every non-depth gather returns in all four texels instead of the sampled value | | `APS5_IMAGE_TABLE_KEY_SCAN` | integer | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | 256 | number of image-table entries the key scan examines | | `APS5_IMAGE_TABLE_STRICT` | presence | `core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp` | | sets image-table materialization to its strict form; 0 leaves it off | diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 1f438c1157..0dc4b1b688 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -124,6 +124,7 @@ - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding throws. In the f16 denormal modes that flush, each denormal input lane is flushed to a zero of its sign when bit 0 of the field is clear (0 and 2), so `inf * 0x0001` is an invalid product there, and when bit 1 is clear (0 and 1) a `v_pk_add_f16` result below 2^-14, or a `v_pk_mul_f16`, `v_pk_fma_f16` or `v_pk_fmac_f16` result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, is flushed to a zero of its sign; `v_pk_min/max_f16` flush only their inputs. The native f16x2 add and multiply is not used while results are flushed. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3, over 19,192 to 162,204 lanes per instruction with op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants and products and sums at the 2^-14 boundary. - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` and the image, buffer, global and flat `fmin/fmax/fcmpswap` flush both operands by the field, see the float atomics entries. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. +- [v_rcp_f32 and v_rcp_iflag_f32 with `APS5_EXACT_RCP`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp) return the hardware reciprocal for normal inputs with normal results: a cubic over 32 mantissa segments, the linear term rounded to nearest even, plus 2,761 one-ULP corrections. It matches all 2^23 mantissas of an RDNA2 (gfx1031) sweep; inputs or results that are not normal keep the driver's 1/x. It is off by default because the coefficient and correction tables add about 13k SPIR-V words to each shader that uses it. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler From 86d9e49f2ee344e3a92b22176b721364413d0430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 23 Oct 2026 22:50:13 +0200 Subject: [PATCH 314/318] docs(shader): note APS5_EXACT_RCP reaches v_rcp_f16 and position W Co-Authored-By: Claude Sonnet 5.5 --- docs/dev/debt/shader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 0dc4b1b688..89f88ad5f4 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -124,7 +124,7 @@ - [Packed f16 NaN results](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (`v_pk_add/mul/fma/min/max_f16`) follow the hardware measured on RDNA2 (gfx1035) at IEEE=0 and IEEE=1 with f16 round-to-nearest-even and f16 denormals kept: with IEEE=1 a signaling NaN input is quieted and min/max return the other operand only for a quiet NaN; with IEEE=0 NaN inputs pass through unquieted and min/max ignore signaling. In `v_pk_fma_f16` an invalid product (inf * 0) gives the default NaN even when src2 is a NaN. A shader without a known float mode is translated as IEEE=0, the mode titles use; a mode with another f16 rounding throws. In the f16 denormal modes that flush, each denormal input lane is flushed to a zero of its sign when bit 0 of the field is clear (0 and 2), so `inf * 0x0001` is an invalid product there, and when bit 1 is clear (0 and 1) a `v_pk_add_f16` result below 2^-14, or a `v_pk_mul_f16`, `v_pk_fma_f16` or `v_pk_fmac_f16` result whose exact value rounded to 11 significant bits with an unbounded exponent is below 2^-14, is flushed to a zero of its sign; `v_pk_min/max_f16` flush only their inputs. The native f16x2 add and multiply is not used while results are flushed. Measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with denorm16 0..3, over 19,192 to 162,204 lanes per instruction with op_sel, op_sel_hi, neg_lo, neg_hi and clamp variants and products and sums at the 2^-14 boundary. - [f32 denormals](../../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) follow the shader's `FLOAT_MODE` f32 denormal field (bits 5:4) when it flushes them (0, and shaders without a known float mode, as in the 0xc0 most titles use), measured on RDNA2 (gfx1035) over 1444 rows per instruction at IEEE=0 and IEEE=1. `v_add/sub/subrev/mul/mul_legacy/mullit/fma/fmac/fmamk/fmaak/min/max/min3/max3/med3/fract/floor/ceil/frexp_mant/sin/cos_f32` flush their f32 inputs and their result to a zero of the same sign (`v_mullit_f32` measured on RDNA2 (gfx1036) over 6907 rows per mode at IEEE=0 and IEEE=1, so a positive denormal src2 counts as zero); the f32 compares, `v_cvt_i32/u32/rpi_i32/flr_i32_f32`, `v_frexp_exp_i32_f32`, `v_cvt_f64_f32`, `v_cvt_f16_f32`, `v_cvt_pkrtz_f16_f32`, `v_cvt_pknorm_i16/u16_f32` and `v_cvt_pk_u8_f32` flush their f32 inputs. `v_div_scale_f32` flushes all three inputs to signed zeros, measured on RDNA2 (gfx1036) over 2744 operand triples per mode at IEEE=0 and IEEE=1 with f32 denormals flushed and kept; its scale-down behaviour and host denormal preservation remain incomplete. `v_div_fixup_f32` flushes its src1 and src2 and keeps a denormal src0 and result, measured on RDNA2 (gfx1036) over 6859 rows per mode at IEEE=0 and IEEE=1. Tininess is detected after rounding, measured on RDNA2 (gfx1036) for `v_mul_f32` and `v_mullit_f32` at IEEE=0 and IEEE=1: a product is flushed when its value rounded to 24 bits with an unbounded exponent is below 2^-126, so `0x3f7ffffe * 0x00800001` (2^-126 * (1 - 2^-46)) gives `0x00800000` and `0x3f7fffff * 0x00800000` (2^-126 * (1 - 2^-24)) gives zero. The check rounds in the f32 round mode of `FLOAT_MODE` (bits 1:0), measured in all four modes, while the products themselves are still rounded to nearest even. This applies to `v_mul_f32`, `v_mul_legacy_f32`, `v_mullit_f32`, and to `v_fma_f32`, `v_fmac_f32`, `v_fmamk_f32`, `v_fmaak_f32` and `v_fma_mix_f32` when the addend is zero; with a nonzero addend the fused result is kept, so an exact result just below 2^-126 that rounds to it is not flushed. In the three directed modes the check is computed exactly from the significands. To nearest even (0, the mode of shaders without a known float mode) one factor is multiplied by 2^64, which is exact below 2^64, and the product with the other factor is compared with 2^-62. That multiply is a plain `OpFMul` like the product, and no f32 rounding mode is requested in the SPIR-V, so the check relies on the driver rounding f32 to nearest even by default, as the products do, and a product that rounds up to 2^-126 is the emitted product. `v_fma_mix_f32` follows this rule with mixed f16/f32 sources too, measured on RDNA2 (gfx1036) with f16 in either multiplicand slot, low and high half selection, and an f32 or f16 zero addend: f32 `0x00801002` times f16 `0x3bff` gives `0x00800000` to nearest at IEEE=0 and IEEE=1, while the neighbours `0x00801001` and `0x00801003` give zero and `0x00800001`. The tiny-product boundary was also measured for both signs in all four f32 round modes at IEEE=0; Its f16 sources are flushed to a zero of their sign when bit 0 of the f16/f64 denormal field is clear (0 and 2), whatever the f32 field, measured on RDNA2 (gfx1031, Radeon RX 6700 XT, AMD Windows driver 32.0.21045.5002 through HIP) at IEEE=0 and IEEE=1 with f32 denormal modes 0 and 3 and f16 denormal modes 0 to 3, over 2744 triples of 14 halves with all three sources f16 and 1960 rows with an f32 multiplicand. `v_fma_mixlo_f16` and `v_fma_mixhi_f16` flush their f32 sources too and keep a product below 2^-126, measured on RDNA2 (gfx1036). `v_mad_legacy_f32` and `v_mac_legacy_f32` flush their inputs, their product and their result in every mode, see its entry; their product is still checked for tininess on its exact value, so one below 2^-126 gives zero even when it rounds to 2^-126, as the console's separately rounded encoding is not measured at this boundary. Denormal modes 1 and 2 throw for the other instructions. Not covered, and not measurable on RDNA2 (no such opcodes) or not measured: `v_mac_f32`, `v_mad_f32`, `v_madak_f32` and `v_madmk_f32` (rounded separately), `v_dot2_f32_f16` and `v_div_fmas_f32` (on hardware `v_div_fmas_f32` flushes only its result), f32 interpolation and `ds_add_f32` keep the previous lowering; `ds_min/max/cmpst_f32` and the image, buffer, global and flat `fmin/fmax/fcmpswap` flush both operands by the field, see the float atomics entries. With f32 denormals kept (3) the results depend on the driver's default denormal behaviour, as no `DenormPreserve` execution mode is requested; `v_ldexp_f32` and the f32 compares are the exceptions. `v_cmp_X_f32` and `v_cmpx_X_f32` compare the operands as sign-magnitude integers with the NaN cases handled apart, so a denormal is not read as zero, measured on RDNA2 (gfx1036) over 256 operand pairs per opcode at IEEE=0 and IEEE=1 with f32 denormals kept and flushed. `v_ldexp_f32` preserves subnormal inputs and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, measured on RDNA2 (gfx1036) over 2163 rows in each of the four round modes at IEEE=0 and IEEE=1. An overflow rounds like any other result (infinity, or the largest finite value when the mode rounds that sign toward zero). With f32 denormals flushed it flushes a subnormal input and result, measured over the same rows in each round mode at IEEE=0 only. A device whose driver splits `Fma` into a multiply and an add flushes a product below 2^-126 before adding a non-zero addend in `v_fma_f32` and `v_fmac_f32`: `0x1fffffff * 0x1fffffff + 0x00800000` gives `0x00800000` instead of the fused `0x00ffffff` on RADV RENOIR (Mesa 26.2.4), whose compiled shader has `v_mad_f32` and no `v_fma_f32`. [F32DenormalFlush](../../../core/libs/prx/libSceAgcDriver/tests/execution/F32DenormalFlush.cpp) accepts that sum on such a device. - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. Measured on RDNA2 (gfx1036) over every f16 input and 32,832 f32 inputs: RADV returns the hardware's bits for every input of rcp, rsq, sqrt and log and all but one of exp (f16 0x11c5, 1 ULP); NVIDIA (615.71.09) stays within 2 ULP except `v_log_f32` near 1.0 (next entry). [v_sin and v_cos](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take the driver's sine or cosine of the radians of the exact cycle, except within 2^-12 of a turn from a zero of the function, where the result is the linear term of the exact distance to the zero: the hardware's constant 6.2831826 (0x40c90fd5) at the integers, which the hardware applies exactly to inputs below 2^-12 of a turn, and the f32 2π at the half and quarter turns, where the hardware is within 7 ULP of it. Outside those bands, a driver result more than 4 ULP from a Taylor series in turns of the exact cycle is replaced by the series, which is within 2 ULP of the true sine and cosine; the hardware is up to 9 ULP from them (gfx1035, 243,092 f32 inputs), so results stay within 13 ULP of the hardware on any driver. RADV returns the hardware's bits for every f16 input and for 32,422 sine and 32,486 cosine results of the 32,832 f32 inputs; the rest are 1 ULP off, except up to 14 ULP on either side of non-zero integers and up to 15 ULP beyond ±256 cycles, where the hardware's own reduction differs from the exact fraction, and up to 6 ULP inside the half- and quarter-turn bands. NVIDIA's own sine is within 1 ULP on 93% of the f32 inputs and up to 1,166 ULP off just outside the bands, its cosine within 163 ULP; the series replaces those results. A denormal f32 input with f32 denormals kept gives the hardware the input times 6.2831826; both hosts flush it. -- [v_rcp_f32 and v_rcp_iflag_f32 with `APS5_EXACT_RCP`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp) return the hardware reciprocal for normal inputs with normal results: a cubic over 32 mantissa segments, the linear term rounded to nearest even, plus 2,761 one-ULP corrections. It matches all 2^23 mantissas of an RDNA2 (gfx1031) sweep; inputs or results that are not normal keep the driver's 1/x. It is off by default because the coefficient and correction tables add about 13k SPIR-V words to each shader that uses it. +- [v_rcp_f32 and v_rcp_iflag_f32 with `APS5_EXACT_RCP`](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterReciprocal.cpp) return the hardware reciprocal for normal inputs with normal results: a cubic over 32 mantissa segments, the linear term rounded to nearest even, plus 2,761 one-ULP corrections. It matches all 2^23 mantissas of an RDNA2 (gfx1031) sweep; inputs or results that are not normal keep the driver's 1/x. It is off by default because the coefficient and correction tables add about 13k SPIR-V words to each shader that uses it. The switch also reaches v_rcp_f16 (rounded from the f32 reciprocal) and the pixel shader's position W input. - [v_ldexp_f32 in denormal modes 1 and 2](../../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the `FLOAT_MODE` f32 denormal field: mode 1 keeps a subnormal input and flushes a result below 2^-126 to a zero of its sign in every f32 round mode, also one that rounds to 2^-126 at subnormal precision (`0x3fffffff` scaled by 2^-127); mode 2 flushes a subnormal input to a zero of its sign and rounds a subnormal result in the f32 round mode of `FLOAT_MODE`, as mode 3 does. Measured on RDNA2 (gfx1030, Radeon RX 6800, AMD Windows driver 32.0.21045.5002 through HIP) over 10,260 rows (54 inputs of both signs: zeros, subnormals, normals near 2^-126 and 1.0, the largest finite value, infinities and NaNs, each scaled by 190 exponents from -2^31 to 2^31-1) at IEEE=0 and IEEE=1 with each f32 denormal mode 0 to 3 and f32 round mode 0 to 3, DX10_CLAMP=1, f16 denormals kept, f16 round to nearest even and FP16_OVFL=0; with the `clamp` modifier the result is clamped as in the other modes. The PS5's gfx1013 is not measured. - [v_log_f32 and v_log_f16](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) keep the driver's log2 unless, for inputs within 1/8 of 1.0, it is more than 4 ULP from an eight-term series in x - 1, which then replaces it. `v_log_f16` goes through the same f32 lowering and is rounded to half. The series is evaluated for every `v_log` and chosen with `OpSelect` rather than a branch, so its seven fused multiply-adds and two multiplies are an unconditional cost. Measured on RDNA2 over all 3,145,728 f32 inputs within 1/8 of 1.0 (gfx1035, IEEE 0, f32 denormals flushed), the hardware is within 1 ULP of the correctly rounded log2 and the series within 2 ULP of it and within 3 ULP of the hardware (2 except at 0x3f752e47, 0x3f7baab0 and 0x3f7bc4fb), so a kept driver value is within 7 ULP of the hardware and a driver that returns the hardware's bits never triggers the gate; RADV (Mesa 26.2.4) returns the hardware's bits on the 9,280 such inputs measured on gfx1036; NVIDIA (615.71.09) was up to 23,947,836 ULP off at 1 - 2^-24 and is within 6 ULP inside the band after the change; outside it its own log2 stays up to 9 ULP off (measured up to 1.375). Over every f16 input, the change moves one `v_log_f16` result on NVIDIA (0x3bed, 1 ULP, onto the hardware's bits) and none on RADV. - [Ordered not-equal, less-than and greater-than compares](../../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) (shader recompiler) - `FPOrdNotEqual32` is emitted as `OpFUnordNotEqual`, and `FPOrdLessThan32` and `FPOrdGreaterThan32` as their ordered forms, each with both operands checked for NaN on their bits; the results are the same for every input. The AMD Windows driver 32.0.13058.8001 (Radeon 860M, RDNA 3.5) returns 1 for a NaN operand when any float form of not-equal feeds a ballot on a 32-wide subgroup (#825), and so does a less-than or greater-than against an infinity the driver sees as a constant (#3536), presumably rewritten into a not-equal by its compiler From c9147cbeecf926ef1f27f01d08f5dda8e69973b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Fri, 23 Oct 2026 22:57:38 +0200 Subject: [PATCH 315/318] test(agc): register only the APS5_EXACT_RCP entry of the rcp hardware test Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index e2f14d2d93..ddcfb73d18 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -2128,6 +2128,7 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + set_target_properties(agc_driver_rcp_hardware_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_driver_rcp_hardware_exact COMMAND agc_driver_rcp_hardware_tests) set_tests_properties(agc_driver_rcp_hardware_exact PROPERTIES ENVIRONMENT "APS5_EXACT_RCP=1;ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 60) if(WIN32) @@ -2427,7 +2428,7 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_locks_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vertex_input_validation_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests) - if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests") + if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests" OR agcTest STREQUAL "agc_driver_rcp_hardware_tests") continue() endif() string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}") From 378ce8f9b526287f5962e8dbaa30695161a4d7c4 Mon Sep 17 00:00:00 2001 From: Akul Tyagi <69496404+AkulTyagi11@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:27:42 +0530 Subject: [PATCH 316/318] test(libSceSaveData.native): cover transferring and v1 mount lifecycle --- core/libs/tests/GuestSaveDataPs4.cpp | 40 ++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 6ee6868bd4..d795b28195 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -8,7 +8,21 @@ #include #include +struct SaveDataMountPs4 { + std::int32_t user_id; + std::int32_t pad; + const void* title_id; + const SceSaveDataDirName* dir_name; + const void* fingerprint; + std::uint64_t blocks; + std::uint32_t mount_mode; + std::uint8_t reserved[32]; +}; + +extern "C" int APS5_VABI sceSaveDataTransferringMount(const SaveDataTransferringMount*, SaveDataMountResult*); extern "C" int APS5_VABI sceSaveDataTransferringMountPs4(const SaveDataTransferringMount*, SaveDataMountResult*); +extern "C" int APS5_VABI sceSaveDataMount(const SaveDataMountPs4*, SaveDataMountResult*); +extern "C" int APS5_VABI sceSaveDataUmount(const SaveDataMountPoint*); extern "C" int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); extern "C" int APS5_VABI sceSaveDataInitialize(const void*); @@ -21,6 +35,9 @@ namespace { constexpr int SaveDataErrorNotFound = -2137063416; constexpr int SaveDataErrorNotInitialized = -2137063423; +constexpr int SaveDataErrorParameter = -2137063424; +constexpr int SaveDataErrorNotMounted = -2137063420; +constexpr std::uint32_t MountReadWrite = 2; int failures = 0; @@ -53,6 +70,11 @@ void TestTransferringMountPs4() { std::memset(&result, 0xAA, sizeof(result)); Check(sceSaveDataTransferringMountPs4(&mount, &result) == SaveDataErrorNotFound, "TransferringMountPs4 returns NOT_FOUND"); Check(result.mount_point.data[0] == '\0', "TransferringMountPs4 reports no mount point"); + std::memset(&result, 0xAA, sizeof(result)); + Check(sceSaveDataTransferringMount(&mount, &result) == SaveDataErrorNotFound, "TransferringMount returns NOT_FOUND"); + Check(result.mount_point.data[0] == '\0', "TransferringMount reports no mount point"); + Check(sceSaveDataTransferringMount(nullptr, nullptr) == SaveDataErrorNotFound, "TransferringMount handles null arguments"); + Check(sceSaveDataTransferringMountPs4(nullptr, nullptr) == SaveDataErrorNotFound, "TransferringMountPs4 handles null arguments"); } void TestDirNameSearchPs4() { @@ -96,6 +118,23 @@ void TestSaveDataMemoryV1() { Check(sceSaveDataTerminate() == 0, "Terminate after v1 Initialize"); } +void TestMountPs4() { + SaveDataMountResult result{}; + Check(sceSaveDataMount(nullptr, &result) == SaveDataErrorParameter, "null mount is PARAMETER"); + SaveDataMountPs4 mount{}; + Check(sceSaveDataMount(&mount, &result) == SaveDataErrorParameter, "null dir_name is PARAMETER"); + Check(sceSaveDataUmount(nullptr) == SaveDataErrorParameter, "umount null point is PARAMETER"); + SceSaveDataDirName dir{}; + std::strcpy(dir.data, "kept"); + mount.dir_name = &dir; + mount.mount_mode = MountReadWrite; + Check(ThrowsRuntimeError([&] { sceSaveDataMount(&mount, nullptr); }), "null mount_result throws"); + Check(sceSaveDataMount(&mount, &result) == 0, "mount existing save directory succeeds"); + Check(result.mount_point.data[0] != '\0', "mount writes mount point"); + Check(sceSaveDataUmount(&result.mount_point) == 0, "umount mounted point succeeds"); + Check(sceSaveDataUmount(&result.mount_point) == SaveDataErrorNotMounted, "umount unmounted point is NOT_MOUNTED"); +} + } int main() { @@ -107,6 +146,7 @@ int main() { TestTransferringMountPs4(); TestDirNameSearchPs4(); TestSaveDataMemoryV1(); + TestMountPs4(); std::filesystem::current_path(previous); std::error_code error; From 39d4e73e2636336896dd4e64c4d97856e178c7f6 Mon Sep 17 00:00:00 2001 From: orezraey Date: Fri, 23 Oct 2026 20:34:40 -0300 Subject: [PATCH 317/318] test(libc): stop pinning the unmeasured reallocalign zero-size realignment Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestReallocalign.cpp | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/core/libs/tests/GuestReallocalign.cpp b/core/libs/tests/GuestReallocalign.cpp index cd36081ec0..f94576018d 100644 --- a/core/libs/tests/GuestReallocalign.cpp +++ b/core/libs/tests/GuestReallocalign.cpp @@ -53,22 +53,5 @@ int main() { void* empty = reallocalign_nid_postfix(nullptr, 0, 0); Require(empty != nullptr); free_nid_postfix(empty); - for (std::size_t attempt = 0; attempt < 64; ++attempt) { - void* loose = reallocalign_nid_postfix(nullptr, 40, 32); - Require(loose != nullptr); - if (reinterpret_cast(loose) % 4096 == 0) { - free_nid_postfix(loose); - continue; - } - bool threw = false; - try { - reallocalign_nid_postfix(loose, 0, 4096); - } catch (const std::invalid_argument&) { - threw = true; - } - Require(threw); - free_nid_postfix(loose); - return 0; - } - throw std::runtime_error("reallocalign check could not get a block without 4096-byte alignment"); + return 0; } From 82b151597a50cd768b21182936d6a9b110b98b8f Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sat, 24 Oct 2026 13:15:57 +1300 Subject: [PATCH 318/318] docs: point game status links to the Game bring-up discussions The README, the Game bring-up discussion form and the converter page now link to the Game bring-up discussions for per-game status and test reports. --- .github/DISCUSSION_TEMPLATE/game-bring-up.yml | 2 +- README.md | 2 +- demo/web/index.html | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/game-bring-up.yml b/.github/DISCUSSION_TEMPLATE/game-bring-up.yml index afb5f3d3d3..ff08c3cca6 100644 --- a/.github/DISCUSSION_TEMPLATE/game-bring-up.yml +++ b/.github/DISCUSSION_TEMPLATE/game-bring-up.yml @@ -6,7 +6,7 @@ body: One thread per title, for every platform: search this category for the title ID first. If a thread exists, reply there, also when your OS or GPU differs from the one it reports. Name the discussion `Game name (title ID)`, for example `DOOM Shareware (PPSA99666)`, without the platform. Bug reports and implementation work belong in issues and pull requests; link them here as the blocker. - Per-game progress is also tracked on the [blocker map](https://alxcbtw.github.io/anyps5-roadmap/), built from public GitHub data. Only share material you have the right to distribute. + Only share material you have the right to distribute. - type: checkboxes id: searched attributes: diff --git a/README.md b/README.md index 3e7fc3ccf4..1a3a9ac30e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully prod ## Compatibility -Per-game progress (stage reached, open blockers, libraries and GPU coverage) is tracked on the [blocker map](https://alxcbtw.github.io/anyps5-roadmap/), a community project built from public GitHub data ([source](https://github.com/AlxcBTW/anyps5-roadmap)). Test reports go in the [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussions, one thread per title. +Per-game status and test reports live in the [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussions, one thread per title. ## Input mapping diff --git a/demo/web/index.html b/demo/web/index.html index e0d468febc..5d974cd58f 100644 --- a/demo/web/index.html +++ b/demo/web/index.html @@ -6,7 +6,7 @@ AnyPS5 · Converter -
+

LOCAL CONVERSION

PS5 apps to Windows/Linux converter

@@ -23,7 +23,7 @@

Memory downloads have a 256 MiB budget, checked against expanded files and output overhead. For multiple ZIPs, the limit applies to the combined bundle; larger batches require Save ZIP to disk in desktop Chrome or Edge. Executable and modules are limited to 256 MiB per app; the icon is also limited to 16 MiB and counts toward the Windows conversion limit. Use the native relinker for larger conversion inputs.

-

Conversion does not guarantee that an app runs. Check the community compatibility list for known blockers and reports.

+

Conversion does not guarantee that an app runs. Check the Game bring-up discussions for known blockers and reports.