From d422c760cdf10401264b669324c813bde4bfa114 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 14 Oct 2026 09:38:31 -0400 Subject: [PATCH 001/131] perf(shader): look up the BDA range of a user-data pointer once at program entry Address-based reads whose 64-bit base is a pair of user-data registers search the BDA table at every read, although the base is one value for the whole draw. Where a program has two or more such reads of one base (at most four bases, those with the most reads), the range holding the base is now searched once at the program's entry, by the subgroup's elected lane when the program already uses subgroup ballots. Each read then takes one branch: when that range holds all of its bytes, and for dword reads the device address is 4-aligned, it loads them directly; otherwise it runs the lookup path as before, so data, faults, invocation stops and wraparound are unchanged. Ranges ending above 0xffffffff00000000, whose base plus a 32-bit offset could wrap, and ranges whose device addresses wrap are not used. The direct path relies on the table being ascending and disjoint, which the driver builds and now requires (BdaAbi::RangesOrdered). AI-assisted: yes --- .../Graphics/src/BdaResources.cpp | 1 + .../libSceAgcDriver/tests/BdaContracts.cpp | 6 + .../libSceAgcDriver/tests/BdaExecution.cpp | 125 +++++++++++ .../prx/libSceAgcDriver/tests/BdaShader.cpp | 87 ++++++++ .../prx/libSceAgcDriver/tests/BdaShader.hpp | 15 ++ .../libSceAgcDriver/tests/ShaderMemory.cpp | 74 ++++++ core/shader/recompiler/BdaAbi.hpp | 8 + core/shader/recompiler/CMakeLists.txt | 1 + .../include/SpirvBackend/SpirvBda.hpp | 6 + .../SpirvBackend/SpirvEmitterState.hpp | 9 + .../SpirvBackend/src/SpirvBdaLookup.cpp | 28 ++- .../SpirvBackend/src/SpirvBdaRead.cpp | 211 ++++++++++++++++++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 28 ++- .../SpirvBackend/src/SpirvModuleEmitter.cpp | 1 + 14 files changed, 591 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 0b94d5e0c9..4bb8b097ed 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -192,6 +192,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo ++it; } } + Require(ShaderRecompiler::BdaAbi::RangesOrdered(ranges.data(), ranges.size()), "BDA table ranges are not ascending and disjoint"); for (const auto& range : ranges) Require((range.permissions & ShaderRecompiler::BdaAbi::Write) == 0 || range.end <= ShaderRecompiler::BdaAbi::WrittenAddressLimit, "a writable BDA range ends past the addresses the written-page record can hold"); table = std::make_shared(context, tableBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); const ShaderRecompiler::BdaAbi::Header header{ShaderRecompiler::BdaAbi::Version, static_cast(ranges.size()), sizeof(ShaderRecompiler::BdaAbi::Range), 0}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index cf54e315dd..dd65417557 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -2,6 +2,7 @@ #include "ControlFlow/RequestSerializer.hpp" #include "SpirvBackend/SpirvBda.hpp" #include +#include namespace { @@ -56,4 +57,9 @@ void RunBdaContractTests() { invalid[0] = invalid[0] == 'A' ? 'B' : 'A'; reject([&] { static_cast(serializer.Deserialize(invalid)); }, "request signature"); reject([&] { static_cast(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version"); + const auto ordered = [](std::initializer_list ranges) { return BdaAbi::RangesOrdered(ranges.begin(), ranges.size()); }; + AgcDriver::Graphics::Require(ordered({}) && ordered({{0x1000, 0x2000, 1, 1, 0}, {0x2000, 0x2000, 1, 1, 0}, {0x2000, 0x3000, 1, 1, 0}, {0x4000, 0x5000, 1, 1, 0}}), "ascending disjoint BDA ranges were refused"); + AgcDriver::Graphics::Require(!ordered({{0x1000, 0x3000, 1, 1, 0}, {0x2000, 0x2100, 1, 1, 0}}), "overlapping BDA ranges were accepted"); + AgcDriver::Graphics::Require(!ordered({{0x4000, 0x5000, 1, 1, 0}, {0x1000, 0x2000, 1, 1, 0}}), "unsorted BDA ranges were accepted"); + AgcDriver::Graphics::Require(!ordered({{0x2000, 0x1000, 1, 1, 0}}), "a BDA range ending before its begin was accepted"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 2804e7e81b..e3a7a3f99d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -213,6 +214,129 @@ static void RunBdaDwordWriteTests(const Context& context) { write(guest + 0x7c, 2, 8, static_cast(0), 64); } +static void RunBdaAnchorTests(const Context& context) { + namespace Abi = ShaderRecompiler::BdaAbi; + constexpr std::uint64_t guest = 0x7fff12340001ULL; + constexpr std::uint64_t limit = 0xffffffff00000000ULL; + constexpr std::uint64_t highest = 0xffffffffffffff00ULL; + constexpr std::uint32_t lanes = 64; + const auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; + Buffer first(context, 3, usage); + Buffer second(context, 2, usage); + Buffer words(context, 16, usage); + Buffer shifted(context, 20, usage); + Buffer large(context, 1024, usage); + Buffer top(context, 16, usage); + Buffer above(context, 16, usage); + std::uint32_t seed = 0x11; + for (auto* buffer : {&first, &second, &words, &shifted, &large, &top, &above}) { + for (std::size_t index = 0; index < buffer->Bytes().size(); ++index) buffer->Bytes()[index] = std::byte(static_cast(seed + index * 7u)); + seed += 0x33; + } + for (std::uint32_t index = 0; index < 16u; ++index) words.Bytes()[index] = std::byte(static_cast(0xa0u + index)); + for (std::uint32_t index = 0; index < 20u; ++index) shifted.Bytes()[index] = std::byte(static_cast(0xc0u + index)); + const std::array ranges{{{guest, guest + 3, first.DeviceAddress(), Abi::Read, 0}, + {guest + 3, guest + 5, second.DeviceAddress(), Abi::Read, 0}, + {guest + 0x100, guest + 0x110, words.DeviceAddress(), Abi::Read, 0}, + {guest + 0x200, guest + 0x210, shifted.DeviceAddress() + 1u, Abi::Read, 0}, + {guest + 0x400, guest + 0x800, large.DeviceAddress(), Abi::Read, 0}, + {limit - 0x10, limit, top.DeviceAddress(), Abi::Read, 0}, + {highest, highest + 0x10, above.DeviceAddress(), Abi::Read, 0}}}; + Require(Abi::RangesOrdered(ranges.data(), ranges.size()), "the BDA anchor test table is not ascending and disjoint"); + Buffer table(context, sizeof(Abi::Header) + sizeof(ranges), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer fault(context, sizeof(Abi::Fault), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + Buffer output(context, (BdaAnchoredReadInputWord + 3u + lanes) * sizeof(std::uint32_t), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + struct Outcome { + Abi::Fault fault{}; + std::array values{}; + }; + std::uint32_t count = static_cast(ranges.size()); + std::uint32_t permissions = Abi::Read; + const auto run = [&](Pipeline& pipeline, std::uint32_t read, std::uint64_t anchor, const std::vector& lows) { + auto copy = ranges; + copy[2].permissions = permissions; + const Abi::Header header{Abi::Version, count, sizeof(Abi::Range), 0}; + std::memcpy(table.Bytes().data(), &header, sizeof(header)); + std::memcpy(table.Bytes().data() + sizeof(header), copy.data(), sizeof(copy)); + std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); + std::memset(output.Bytes().data(), 0xa5, BdaAnchoredReadInputWord * sizeof(std::uint32_t)); + std::vector inputs{static_cast(anchor), static_cast(anchor >> 32u), read}; + inputs.insert(inputs.end(), lows.begin(), lows.end()); + std::memcpy(output.Bytes().data() + BdaAnchoredReadInputWord * sizeof(std::uint32_t), inputs.data(), inputs.size() * sizeof(std::uint32_t)); + pipeline.Run(1); + Outcome outcome; + std::memcpy(&outcome.fault, fault.Bytes().data(), sizeof(outcome.fault)); + std::memcpy(outcome.values.data(), output.Bytes().data(), sizeof(outcome.values)); + return outcome; + }; + struct Programs { + Programs(const Context& context, const BdaAnchoredReadProgram& reads, const std::array& buffers) : reads(reads), plain(context, MakeBdaAnchoredReadTestShader(reads, false), buffers), anchored(context, MakeBdaAnchoredReadTestShader(reads, true), buffers) {} + BdaAnchoredReadProgram reads; + Pipeline plain; + Pipeline anchored; + }; + const auto build = [&](const BdaAnchoredReadProgram& reads) { return std::make_unique(context, reads, std::array{&table, &fault, &output}); }; + const auto compare = [&](Programs& programs, std::initializer_list anchors, const std::vector& lows) { + std::vector> batches; + if (programs.reads.lanes == 1u) { + for (const auto low : lows) batches.push_back({low}); + } else { + batches.push_back(lows); + batches.back().resize(programs.reads.lanes, lows.back()); + } + for (std::uint32_t index = 0; index < programs.reads.reads.size(); ++index) { + const auto& read = programs.reads.reads[index]; + for (const auto anchor : anchors) { + for (const auto& batch : batches) { + const auto expected = run(programs.plain, index, anchor, batch); + const auto actual = run(programs.anchored, index, anchor, batch); + const bool faultsMatch = programs.reads.lanes == 1u ? std::memcmp(&expected.fault, &actual.fault, sizeof(expected.fault)) == 0 : expected.fault.state == actual.fault.state; + Require(faultsMatch && expected.values == actual.values, "an anchored BDA read differs from the same read through the lookups at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(batch[0]) + " + (" + std::to_string(read.immediate) + "), " + std::to_string(read.bits) + " bits, " + std::to_string(read.dwords) + " dwords"); + } + } + } + }; + std::vector reads; + for (const std::int32_t immediate : {0, 4, -1, -8, 0x7ffffff0, std::numeric_limits::min()}) { + for (const auto& [bits, dwords, used] : std::initializer_list>{{8, 1, 1}, {16, 1, 1}, {32, 1, 1}, {32, 4, 15}, {32, 4, 10}, {32, 3, 4}}) reads.push_back({immediate, bits, dwords, used}); + } + const auto anchors = {guest + 0x100, guest + 0x108, guest, guest + 3, guest + 0x200, guest - 1, guest + 0x110, guest + 0x400, limit - 0x10, limit - 4, highest, highest + 8, std::numeric_limits::max() - 2}; + std::vector lows{0u, 1u, 2u, 3u, 4u, 8u, 0xcu, 0xdu, 0xeu, 0xfu, 0x10u, 0x14u, 0x100u, 0x108u, 0x10cu, 0x1fcu, 0x200u, 0x20eu, 0x3f0u, 0x3fcu, 0x400u, 0x7fffu, 0x7fffffffu, 0x80000000u, 0xfffffff0u, 0xfffffffcu, 0xffffffffu}; + for (std::uint32_t lane = 0; lane < 32u; ++lane) lows.push_back(lane * 0x24u); + std::vector strided; + for (std::uint32_t lane = 0; lane < lanes; ++lane) strided.push_back(lane * 16u); + auto continuing = build({reads, false, false, false, lanes}); + compare(*continuing, anchors, lows); + compare(*continuing, {guest + 0x400}, strided); + auto elected = build({reads, false, true, false, lanes}); + compare(*elected, anchors, lows); + compare(*elected, {guest + 0x400}, strided); + auto coherent = build({{reads[0], reads[2], reads[9], reads[16], reads[22]}, false, false, true, lanes}); + compare(*coherent, anchors, lows); + auto stopping = build({reads, true, true, false, 1}); + compare(*stopping, anchors, {0u, 0x80000000u, 0xfffffffcu}); + const auto check = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { return run(stopping->anchored, read, anchor, {low}); }; + const auto wide = check(3, guest + 0x100, 0); + Require(wide.fault.state == Abi::FaultState::Empty && wide.values[0] == 0xa3a2a1a0u && wide.values[1] == 0xa7a6a5a4u && wide.values[2] == 0xabaaa9a8u && wide.values[3] == 0xafaeadacu, "an anchored span read returned wrong data"); + const auto some = check(4, guest + 0x100, 0); + Require(some.fault.state == Abi::FaultState::Empty && some.values[0] == 0u && some.values[1] == 0xa7a6a5a4u && some.values[2] == 0u && some.values[3] == 0xafaeadacu, "an anchored read of some dwords returned wrong data"); + const auto odd = check(2, guest + 0x200, 0); + Require(odd.fault.state == Abi::FaultState::Empty && odd.values[0] == 0xc4c3c2c1u, "an anchored read of an odd device address returned wrong data"); + const auto byte = check(0, guest + 0x200, 0xd); + Require(byte.fault.state == Abi::FaultState::Empty && byte.values[0] == 0xceu, "an anchored byte read returned wrong data"); + const auto below = check(20, guest + 0x108, 4); + Require(below.fault.state == Abi::FaultState::Empty && below.values[0] == 0xa7a6a5a4u, "an anchored read below its base returned wrong data"); + const auto unmapped = check(2, guest + 0x100, 0x10); + Require(unmapped.fault.state == Abi::FaultState::Ready && unmapped.fault.reason == Abi::FaultReason::Unmapped && unmapped.fault.address == guest + 0x110 && unmapped.fault.instruction == 0x1234 && unmapped.values[0] == 0xa5a5a5a5u, "an anchored read past its range did not fault"); + permissions = 0; + compare(*stopping, {guest + 0x100}, {0u, 4u}); + compare(*elected, {guest + 0x100}, lows); + permissions = Abi::Read; + count = 3; + compare(*stopping, {guest + 0x100, guest}, {0u, 4u}); + compare(*elected, {guest + 0x100, guest}, lows); +} + void RunBdaExecutionTests(const Context& context) { namespace Abi = ShaderRecompiler::BdaAbi; Buffer first(context, 3, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT); @@ -333,6 +457,7 @@ void RunBdaExecutionTests(const Context& context) { ranges[0].permissions = 0; run(guest, 8, 0, Abi::FaultReason::Permission); RunBdaDwordWriteTests(context); + RunBdaAnchorTests(context); } void RunBdaStoreExecutionTests(const Context& context) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index 60a2e87853..a24dd6a955 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -133,3 +133,90 @@ std::vector MakeBdaWriteTestShader(std::uint64_t address, std::ui #endif return shader; } + +std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored) { + using namespace ShaderRecompiler; + IrProgram program; + program.Resources().stage = IrShaderStage::Compute; + program.Info().usesDma = true; + if (reads.coherent) { + MemoryInfo memory; + memory.coherent = true; + program.Resources().memoryInfo = {memory}; + } + SpirvEmitterState state(program, {}); + EmitBaseHeader(state.module, program); + if (reads.elect) { + state.module.EmitCapability(spv::CapabilityGroupNonUniform); + state.module.EmitCapability(spv::CapabilityGroupNonUniformBallot); + state.requirements.subgroupBallot = true; + } + const auto define = [&](std::uint32_t binding) { + const auto variable = state.module.DefineGlobalVariable(TypeStorageBufferPointer(state), spv::StorageClassStorageBuffer); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationDescriptorSet, 0u); + state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationBinding, binding); + return variable; + }; + state.bdaPagetableVariable = define(0); + state.faultBufferVariable = define(1); + const auto output = define(2); + std::vector interfaceVariables{state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassInput, TypeU32(state)), spv::StorageClassInput)}; + state.module.AddAnnotation(spv::OpDecorate, interfaceVariables[0], spv::DecorationBuiltIn, spv::BuiltInLocalInvocationIndex); + state.bdaStopsInvocations = reads.stops; + DefineGetBdaPointer(state); + if (anchored) DefineBdaAnchorFunction(state); + const auto main = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state)); + EmitLabel(state, state.module.AllocateId()); + SpirvValueEmitContext ctx(state); + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto input = [&](std::uint32_t word) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, value, BdaWord(state, output, word)); + return value; + }; + const auto base = Binary(state, spv::OpBitwiseOr, u64, Unary(state, spv::OpUConvert, u64, input(ConstantU32(state, BdaAnchoredReadInputWord))), Binary(state, spv::OpShiftLeftLogical, u64, Unary(state, spv::OpUConvert, u64, input(ConstantU32(state, BdaAnchoredReadInputWord + 1u))), BdaConstant(state, 32u))); + SpirvEmitterState::BdaAnchor anchor{}; + if (anchored) EmitBdaAnchorRange(state, anchor, base); + const auto selector = input(ConstantU32(state, BdaAnchoredReadInputWord + 2u)); + const auto laneIndex = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, u32, laneIndex, interfaceVariables[0]); + const auto low = input(Binary(state, spv::OpIAdd, u32, laneIndex, ConstantU32(state, BdaAnchoredReadInputWord + 3u))); + const auto slot = Binary(state, spv::OpIMul, u32, laneIndex, ConstantU32(state, 4u)); + IrBuilder ir(program); + for (std::uint32_t index = 0; index < reads.reads.size(); ++index) { + const auto& read = reads.reads[index]; + const auto opcode = read.bits == 8u ? IrOpcode::LoadAddressU8 : read.bits == 16u ? IrOpcode::LoadAddressU16 : read.dwords == 2u ? IrOpcode::LoadAddressU32x2 : read.dwords == 3u ? IrOpcode::LoadAddressU32x3 : read.dwords == 4u ? IrOpcode::LoadAddressU32x4 : IrOpcode::LoadAddressU32; + const auto type = read.bits != 32u || read.dwords == 1u ? IrType::U32 : read.dwords == 2u ? IrType::U32x2 : read.dwords == 3u ? IrType::U32x3 : IrType::U32x4; + auto& instruction = program.CreateValue(opcode, type); + MemoryFlags flags{}; + flags.pc = 0x1234; + instruction.SetFlags(flags); + if (read.bits == 32u && read.dwords > 1u) { + const auto extract = read.dwords == 2u ? IrOpcode::CompositeExtractU32x2 : read.dwords == 3u ? IrOpcode::CompositeExtractU32x3 : IrOpcode::CompositeExtractU32x4; + for (std::uint32_t dword = 0; dword < read.dwords; ++dword) { + if ((read.used & (1u << dword)) == 0u) continue; + auto& user = program.CreateValue(extract, IrType::U32); + user.AddArgument(&instruction); + user.AddArgument(&ir.Constant(dword)); + } + } + EmitIfCondition(state, Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, index)), [&] { + const auto lookup = [&] { + const auto address = AddBdaAddress(ctx, instruction, base, Unary(state, spv::OpUConvert, u64, low), false); + if (read.bits == 32u) return EmitBdaDwordReads(ctx, instruction, address, static_cast(read.immediate), read.dwords); + return std::array{EmitBdaRead(ctx, instruction, AddBdaImmediate(ctx, instruction, address, read.immediate), read.bits)}; + }; + const auto values = anchored ? EmitAnchoredBdaRead(state, instruction, anchor, low, read.immediate, read.bits, read.dwords, 0u, lookup) : lookup(); + for (std::uint32_t dword = 0; dword < (read.bits == 32u ? read.dwords : 1u); ++dword) { + state.module.AddFunction(spv::OpStore, BdaWord(state, output, Binary(state, spv::OpIAdd, u32, slot, ConstantU32(state, dword))), values[dword]); + } + }); + } + state.module.AddFunction(spv::OpReturn); + state.module.AddFunction(spv::OpFunctionEnd); + state.module.AddExecutionMode(main, spv::ExecutionModeLocalSize, reads.lanes, 1u, 1u); + state.module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", interfaceVariables); + return state.module.Finalize(); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index 62a3adcb19..ec04f67aba 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -11,6 +11,21 @@ std::vector MakeBdaSpanReadTestShader(std::uint64_t address, std: std::vector MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values); std::vector MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); std::vector MakeBdaWriteTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent); +inline constexpr std::uint32_t BdaAnchoredReadInputWord = 256; +struct BdaAnchoredRead { + std::int32_t immediate = 0; + std::uint32_t bits = 32; + std::uint32_t dwords = 1; + std::uint32_t used = 1; +}; +struct BdaAnchoredReadProgram { + std::vector reads; + bool stops = true; + bool elect = false; + bool coherent = false; + std::uint32_t lanes = 1; +}; +std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored); void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaContractTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 9021647520..9cded9c715 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3666,6 +3666,79 @@ void verifyBdaReadFallbackFunctions() { } } +void verifyBdaAnchors() { + using namespace ShaderRecompiler; + const std::array capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + alignas(256) static std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array userData{0x10000000u, 0u, 0u, 0u, static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 128u, 0x01016facu}; + const auto compile = [&](std::uint32_t scalarBase, std::uint32_t stride) { + std::vector code{0x7e020200u, 0x7e040201u}; + for (std::uint32_t load = 0; load < scalarBase; ++load) { + code.push_back(0xdc308000u | (stride * load + 4u)); + code.push_back(((3u + load) << 24u) | 0x00040000u); + } + code.push_back(0xdc308000u | 4u); + code.push_back(((3u + scalarBase) << 24u) | 0x007d0001u); + code.push_back(0xbf8c3f70u); + for (std::uint32_t load = 1; load <= scalarBase; ++load) code.push_back(0x4a060103u | ((3u + load) << 9u)); + code.insert(code.end(), {0xe0700000u, 0x80010300u, 0xbf810000u}); + RecompileRequest request{}; + request.shader = {ShaderStage::Compute, 0x60000u, 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.bdaAbiVersion = BdaAbi::Version; + request.target.supportedCapabilities = capabilities; + request.target.supportedExtensions = extensions; + request.target.fragmentShaderBarycentricEnabled = false; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return Recompile(request).spirv; + }; + for (const std::uint32_t stride : {4u, 32u}) { + for (const std::uint32_t loads : {0u, 1u, 2u, 4u}) { + const bool anchored = loads >= 2u; + const auto words = compile(loads, stride); + std::map names; + std::string function; + std::map mainCalls; + std::size_t anchorFunctions = 0; + std::size_t branchesBeforeAnchor = 0; + std::size_t directLoads = 0; + for (std::size_t cursor = 5; cursor < words.size();) { + const auto length = words[cursor] >> 16u; + require(length != 0 && length <= words.size() - cursor, "BDA anchors: truncated SPIR-V instruction"); + const auto op = words[cursor] & 0xffffu; + if (op == spv::OpName) names[words[cursor + 1]] = reinterpret_cast(&words[cursor + 2]); + if (op == spv::OpFunction) { + function = names[words[cursor + 2]]; + if (function == "bda_anchor_range") ++anchorFunctions; + } + if (op == spv::OpFunctionCall && function == "main") { + const auto& callee = names[words[cursor + 3]]; + if (callee == "bda_anchor_range") require(mainCalls.empty(), "BDA anchors: the anchor is searched after a read"); + ++mainCalls[callee.starts_with("read_bda_span") ? std::string("read_bda_span") : callee]; + } + if (op == spv::OpBranchConditional && function == "main" && mainCalls["bda_anchor_range"] == 0u) ++branchesBeforeAnchor; + if (op == spv::OpLoad && function == "main" && length > 4u && words[cursor + 4] == spv::MemoryAccessAlignedMask && words[cursor + 5] == 4u) ++directLoads; + cursor += length; + } + const auto spans = loads == 0u ? 1u : stride == 4u ? 2u : loads + 1u; + require(anchorFunctions == (anchored ? 1u : 0u), "BDA anchors: the anchor search is defined without anchored reads, or twice"); + require(mainCalls["bda_anchor_range"] == (anchored ? 1u : 0u), "BDA anchors: the reads of one user data base do not share one anchor"); + require(!anchored || branchesBeforeAnchor == 0u, "BDA anchors: the anchor is not searched at the program's entry"); + require(mainCalls["read_bda_span"] == spans, "BDA anchors: a read lost its lookup path"); + require(directLoads == (anchored ? loads : 0u), "BDA anchors: an anchored read does not load its dwords directly"); + } + } +} + void verifyFunctionLdsBound() { using namespace ShaderRecompiler; const auto build = [](const auto& body) { @@ -4278,6 +4351,7 @@ int main(int argc, char** argv) { verifyWaveUniformValues(); verifyTwoLaneUniformValues(); verifyBdaReadFallbackFunctions(); + verifyBdaAnchors(); verifyFunctionLdsBound(); verifyFunctionLdsUpdates(); verifySrtReadPatching(); diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index f964d7fe99..bdb3049862 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -27,6 +27,14 @@ struct Range { std::uint32_t reserved; }; +inline bool RangesOrdered(const Range* ranges, std::size_t count) { + for (std::size_t index = 0; index < count; ++index) { + if (ranges[index].end < ranges[index].begin) return false; + if (index != 0 && ranges[index].begin < ranges[index - 1].end) return false; + } + return true; +} + enum class FaultState : std::uint32_t { Empty, Writing, Ready }; enum class FaultReason : std::uint32_t { Unmapped = 1, Permission, Overflow, InvalidTable, InvalidRectangle, LoopLimit, Unaligned, InvalidDescriptor }; diff --git a/core/shader/recompiler/CMakeLists.txt b/core/shader/recompiler/CMakeLists.txt index 7598e4aad9..9b1dcd0233 100644 --- a/core/shader/recompiler/CMakeLists.txt +++ b/core/shader/recompiler/CMakeLists.txt @@ -217,6 +217,7 @@ function(add_shader_recompiler target runtimeTarget) string(APPEND shaderCacheExtra " system-deps=${ANYPS5_SYSTEM_DEPS_STAMP}") endif() set(shaderCacheReviewedNames + BdaAbi::RangesOrdered BuildBarycentricGeometryShader BuildPreparedShaderKey BuildRectListShaders diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 47ffeb796c..67b486dd52 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -38,6 +38,12 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false, std::uint32_t usedDwords = 0u); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); +void CollectBdaAnchors(SpirvEmitterState& state); +void DefineBdaAnchorFunction(SpirvEmitterState& state); +void EmitBdaAnchors(SpirvEmitterState& state); +void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& anchor, std::uint32_t base); +const SpirvEmitterState::BdaAnchor* FindBdaAnchor(const SpirvEmitterState& state, const IrValue& inst); +std::array EmitAnchoredBdaRead(SpirvEmitterState& state, const IrValue& inst, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bits, std::uint32_t dwords, std::uint32_t usedDwords, const std::function()>& lookup); std::uint32_t AddBdaImmediate(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::int32_t immediate); } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index afd26653ac..b9d8ba6436 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -129,6 +129,15 @@ struct SpirvEmitterState { std::array, 2> bdaDwordReadFunctions {}; std::array, 2> bdaSpanReadFunctions {}; std::unordered_map> bdaGroupedLoads; + struct BdaAnchor { + std::uint32_t lowRegister = 0; + std::uint32_t highRegister = 0; + std::uint32_t size = 0; + std::uint32_t device = 0; + std::uint32_t offset = 0; + }; + std::vector bdaAnchors; + std::uint32_t bdaAnchorFunction = 0; std::uint32_t bdaStopValue = 0; std::uint32_t bdaPcOverride = 0; // False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop). diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index b48e8775b5..55172995b1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -10,18 +10,18 @@ namespace ShaderRecompiler { namespace { -void ReturnBdaZeroIf(SpirvEmitterState& state, std::uint32_t condition) { +void ReturnBdaZeroIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t zero) { const auto failed = state.module.AllocateId(); const auto next = state.module.AllocateId(); state.module.AddFunction(spv::OpSelectionMerge, next, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, condition, failed, next); EmitLabel(state, failed); - state.module.AddFunction(spv::OpReturnValue, BdaConstant(state, 0u)); + state.module.AddFunction(spv::OpReturnValue, zero); EmitLabel(state, next); } // (u64 address, u32 bytes, u32 instruction) -> u64 device address of the range holding all the bytes, -std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read) { +std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool recordFaults, std::uint32_t permission = BdaAbi::Read, bool rangeResult = false) { const auto u32 = TypeU32(state); const auto u64 = TypeScalarU64(state); const auto boolean = TypeBool(state); @@ -29,7 +29,10 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r const auto binary = [&](std::uint32_t op, std::uint32_t type, std::uint32_t left, std::uint32_t right) { return Binary(state, op, type, left, right); }; const auto function = state.module.AllocateId(); state.module.AddName(function, name); - state.module.AddFunction(spv::OpFunction, u64, function, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, u64, u64, u32, u32)); + const auto rangeType = rangeResult ? state.module.Type(spv::OpTypeVector, u64, 3u) : 0u; + const auto resultType = rangeResult ? rangeType : u64; + const auto zero = rangeResult ? state.module.Constant(spv::OpConstantComposite, rangeType, BdaConstant(state, 0u), BdaConstant(state, 0u), BdaConstant(state, 0u)) : BdaConstant(state, 0u); + state.module.AddFunction(spv::OpFunction, resultType, function, rangeResult ? spv::FunctionControlMaskNone : spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, resultType, u64, u32, u32)); const auto address = state.module.AllocateId(); const auto bytes = state.module.AllocateId(); const auto instruction = state.module.AllocateId(); @@ -41,7 +44,7 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r if (recordFaults) { ReturnBdaFailureIf(state, condition, address, bytes, instruction, reason); } else { - ReturnBdaZeroIf(state, condition); + ReturnBdaZeroIf(state, condition, zero); } }; const auto failFirst = [&](std::initializer_list> checks) { @@ -51,7 +54,7 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r } const auto failed = binary(spv::OpINotEqual, boolean, reason, constant(0u)); if (!recordFaults) { - ReturnBdaZeroIf(state, failed); + ReturnBdaZeroIf(state, failed, zero); return; } const auto failedLabel = state.module.AllocateId(); @@ -124,6 +127,13 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r {binary(spv::OpULessThan, boolean, result, base), BdaAbi::FaultReason::Overflow}, {binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow}, }); + if (rangeResult) { + const auto range = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, rangeType, range, begin, finish, base); + state.module.AddFunction(spv::OpReturnValue, range); + state.module.AddFunction(spv::OpFunctionEnd); + return function; + } state.module.AddFunction(spv::OpReturnValue, result); state.module.AddFunction(spv::OpFunctionEnd); return function; @@ -212,6 +222,10 @@ static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { return function; } +void DefineBdaAnchorFunction(SpirvEmitterState& state) { + if (state.bdaAnchorFunction == 0) state.bdaAnchorFunction = DefineBdaLookup(state, "bda_anchor_range", false, BdaAbi::Read, true); +} + void DefineGetBdaPointer(SpirvEmitterState& state) { if (state.faultBufferVariable != 0) DefineBdaFaultFunction(state); if (!state.program.Info().usesDma) return; @@ -219,6 +233,8 @@ void DefineGetBdaPointer(SpirvEmitterState& state) { if (!BdaByteReadsForced()) state.bdaProbeFunction = DefineBdaLookup(state, "probe_bda_pointer", false); DefineBdaDwordReadFunctions(state); DefineBdaSpanReadFunctions(state); + CollectBdaAnchors(state); + if (!state.bdaAnchors.empty()) DefineBdaAnchorFunction(state); if (state.program.Info().bdaWrites) { state.bdaWritePointerFunction = DefineBdaLookup(state, "get_bda_write_pointer", true, BdaAbi::Write); if (!BdaByteReadsForced()) state.bdaWriteProbeFunction = DefineBdaLookup(state, "probe_bda_write_pointer", false, BdaAbi::Write); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index b43f89f997..73a196bfc6 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -1,12 +1,16 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include "SpirvBackend/SpirvEmitterInstructions.hpp" #include #include +#include #include #include +#include #include #include +#include namespace ShaderRecompiler { @@ -417,6 +421,213 @@ bool BdaInvocationsMayStop(const IrProgram& program) { return true; } +namespace { + +constexpr std::size_t MaxBdaAnchors = 4; +constexpr std::uint64_t BdaAnchorAddressLimit = ~std::uint64_t{0} << 32u; + +std::optional> UserDataBase(const IrProgram& program, const IrValue& inst) { + if (AddressOpcodeInfoOf(inst.Opcode()).access != AddressAccess::Read || inst.ArgumentCount() < 2u) return std::nullopt; + const auto index = inst.Flags().index; + const auto& memory = program.Resources().memoryInfo; + if (index >= memory.size()) return std::nullopt; + const auto& info = memory[index]; + if (info.kind != ResourceKind::ScalarAddress && info.kind != ResourceKind::Flat && info.kind != ResourceKind::Global) return std::nullopt; + if (info.planningOnly || info.addressIsFull || (info.kind == ResourceKind::Flat && program.Resources().stage == IrShaderStage::Compute)) return std::nullopt; + const IrValue* handle = inst.Argument(0) != nullptr ? inst.Argument(0)->Resolve() : nullptr; + if (handle == nullptr || handle->Opcode() != IrOpcode::GetAddressResource || handle->ArgumentCount() != 2u) return std::nullopt; + const auto userData = [](const IrValue* value) -> std::optional { + value = value != nullptr ? value->Resolve() : nullptr; + if (value == nullptr || value->Opcode() != IrOpcode::GetUserData || value->ArgumentCount() == 0u || value->Argument(0) == nullptr) return std::nullopt; + return value->Argument(0)->Register().index; + }; + const auto low = userData(handle->Argument(0)); + const auto high = userData(handle->Argument(1)); + if (!low || !high) return std::nullopt; + return std::pair{*low, *high}; +} + +std::uint32_t EmitElectedBdaRange(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, const std::function& search) { + const auto elected = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformElect, TypeBool(state), elected, ConstantU32(state, spv::ScopeSubgroup)); + const auto before = state.currentLabel; + const auto electedLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, elected, electedLabel, merge); + EmitLabel(state, electedLabel); + const auto found = search(); + const auto electedExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, type, value, found, electedExit, zero, before); + const auto u64 = TypeScalarU64(state); + const auto pair = TypeU32Vector(state, 2); + std::array parts{}; + for (std::uint32_t component = 0; component < parts.size(); ++component) { + const auto part = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u64, part, value, component); + const auto shared = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBroadcastFirst, pair, shared, ConstantU32(state, spv::ScopeSubgroup), Unary(state, spv::OpBitcast, pair, part)); + parts[component] = Unary(state, spv::OpBitcast, u64, shared); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, type, result, parts[0], parts[1], parts[2]); + return result; +} + +std::uint32_t BdaAnchorHolds(SpirvEmitterState& state, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bytes, std::uint32_t& translated) { + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + auto offset = Binary(state, spv::OpIAdd, u64, anchor.offset, Unary(state, spv::OpUConvert, u64, low)); + if (immediate != 0) offset = Binary(state, spv::OpIAdd, u64, offset, BdaConstant(state, static_cast(static_cast(immediate)))); + const auto width = BdaConstant(state, bytes); + const auto fits = Binary(state, spv::OpUGreaterThanEqual, boolean, anchor.size, width); + const auto inside = Binary(state, spv::OpULessThanEqual, boolean, offset, Binary(state, spv::OpISub, u64, anchor.size, width)); + translated = Binary(state, spv::OpIAdd, u64, anchor.device, offset); + return Binary(state, spv::OpLogicalAnd, boolean, fits, inside); +} + +} + +void CollectBdaAnchors(SpirvEmitterState& state) { + state.bdaAnchors.clear(); + if (!state.program.Info().usesDma || state.bdaProbeFunction == 0) return; + std::vector, std::size_t>> bases; + for (const IrBlock* block : state.program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + const auto base = UserDataBase(state.program, *inst); + if (!base) continue; + const auto found = std::find_if(bases.begin(), bases.end(), [&](const auto& entry) { return entry.first == *base; }); + if (found != bases.end()) ++found->second; + else bases.push_back({*base, 1u}); + } + } + std::stable_sort(bases.begin(), bases.end(), [](const auto& left, const auto& right) { return left.second > right.second; }); + for (const auto& [base, reads] : bases) { + if (reads < 2u || state.bdaAnchors.size() == MaxBdaAnchors) break; + state.bdaAnchors.push_back({base.first, base.second}); + } +} + +void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& anchor, std::uint32_t base) { + if (state.bdaAnchorFunction == 0) throw std::runtime_error("the BDA anchor search function is missing"); + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + const auto rangeType = state.module.Type(spv::OpTypeVector, u64, 3u); + const auto search = [&] { + const auto range = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, rangeType, range, state.bdaAnchorFunction, base, ConstantU32(state, 1u), ConstantU32(state, 0u)); + return range; + }; + const auto zero = state.module.Constant(spv::OpConstantComposite, rangeType, BdaConstant(state, 0u), BdaConstant(state, 0u), BdaConstant(state, 0u)); + const auto range = state.requirements.subgroupBallot ? EmitElectedBdaRange(state, rangeType, zero, search) : search(); + const auto extract = [&](std::uint32_t component) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, u64, value, range, component); + return value; + }; + const auto begin = extract(0u); + const auto end = extract(1u); + anchor.device = extract(2u); + anchor.offset = Binary(state, spv::OpISub, u64, base, begin); + const auto span = Binary(state, spv::OpISub, u64, end, begin); + const auto deviceEnd = Binary(state, spv::OpIAdd, u64, anchor.device, span); + auto usable = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpINotEqual, boolean, anchor.device, BdaConstant(state, 0u)), Binary(state, spv::OpUGreaterThan, boolean, deviceEnd, anchor.device)); + usable = Binary(state, spv::OpLogicalAnd, boolean, usable, Binary(state, spv::OpULessThanEqual, boolean, end, BdaConstant(state, BdaAnchorAddressLimit))); + anchor.size = Select(state, u64, usable, span, BdaConstant(state, 0u)); +} + +void EmitBdaAnchors(SpirvEmitterState& state) { + const auto u64 = TypeScalarU64(state); + for (auto& anchor : state.bdaAnchors) { + const auto low = Unary(state, spv::OpUConvert, u64, EmitGetUserData(state, static_cast(anchor.lowRegister))); + const auto high = Unary(state, spv::OpUConvert, u64, EmitGetUserData(state, static_cast(anchor.highRegister))); + EmitBdaAnchorRange(state, anchor, Binary(state, spv::OpBitwiseOr, u64, low, Binary(state, spv::OpShiftLeftLogical, u64, high, BdaConstant(state, 32u)))); + } +} + +const SpirvEmitterState::BdaAnchor* FindBdaAnchor(const SpirvEmitterState& state, const IrValue& inst) { + if (state.bdaAnchors.empty()) return nullptr; + const auto base = UserDataBase(state.program, inst); + if (!base) return nullptr; + for (const auto& anchor : state.bdaAnchors) { + if (anchor.lowRegister == base->first && anchor.highRegister == base->second && anchor.size != 0u) return &anchor; + } + return nullptr; +} + +std::array EmitAnchoredBdaRead(SpirvEmitterState& state, const IrValue& inst, const SpirvEmitterState::BdaAnchor& anchor, std::uint32_t low, std::int32_t immediate, std::uint32_t bits, std::uint32_t dwords, std::uint32_t usedDwords, const std::function()>& lookup) { + if (bits != 8u && bits != 16u && bits != 32u) throw std::runtime_error("unsupported anchored BDA read width"); + if (bits == 32u && (dwords == 0u || dwords > 4u)) throw std::runtime_error("unsupported anchored BDA read dword count"); + const auto used = bits != 32u ? 1u : usedDwords != 0u ? usedDwords : UsedBdaDwords(inst, dwords); + if (used == 0u) return lookup(); + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto boolean = TypeBool(state); + std::uint32_t first = 0; + std::uint32_t last = 0; + std::uint32_t bytes = bits / 8u; + if (bits == 32u) { + first = static_cast(std::countr_zero(used)); + last = 31u - static_cast(std::countl_zero(used)); + bytes = (last - first + 1u) * 4u; + } + std::uint32_t translated = 0; + auto direct = BdaAnchorHolds(state, anchor, low, static_cast(static_cast(immediate) + first * 4u), bytes, translated); + if (bits == 32u) direct = Binary(state, spv::OpLogicalAnd, boolean, direct, Unary(state, spv::OpLogicalNot, boolean, BdaAddressUnaligned(state, translated))); + const auto directLabel = state.module.AllocateId(); + const auto lookupLabel = state.module.AllocateId(); + const auto merge = state.module.AllocateId(); + state.module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, direct, directLabel, lookupLabel); + EmitLabel(state, directLabel); + std::array directValues{}; + const bool coherent = BdaCoherent(state, inst); + if (bits == 32u) { + for (std::uint32_t dword = first; dword <= last; ++dword) { + if ((used & (1u << dword)) == 0u) continue; + const auto element = dword == first ? translated : Binary(state, spv::OpIAdd, u64, translated, BdaConstant(state, (dword - first) * 4u)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, TypePhysicalU32Pointer(state), pointer, element); + directValues[dword] = state.module.AllocateId(); + if (coherent) state.module.AddFunction(spv::OpAtomicLoad, u32, directValues[dword], pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, 0u)); + else state.module.AddFunction(spv::OpLoad, u32, directValues[dword], pointer, BdaAccessMask(false), 4u); + } + } else { + const auto byteType = state.module.Type(spv::OpTypeInt, 8u, 0u); + const auto bytePointer = TypePointer(state, spv::StorageClassPhysicalStorageBuffer, byteType); + auto value = ConstantU32(state, 0u); + for (std::uint32_t byte = 0; byte < bytes; ++byte) { + const auto element = byte == 0u ? translated : Binary(state, spv::OpIAdd, u64, translated, BdaConstant(state, byte)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpConvertUToPtr, bytePointer, pointer, element); + const auto loaded = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, byteType, loaded, pointer, BdaAccessMask(coherent), 1u); + value = Binary(state, spv::OpBitwiseOr, u32, value, Binary(state, spv::OpShiftLeftLogical, u32, Unary(state, spv::OpUConvert, u32, loaded), ConstantU32(state, byte * 8u))); + } + directValues[0] = value; + } + const auto directExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, lookupLabel); + const auto lookupValues = lookup(); + const auto lookupExit = state.currentLabel; + state.module.AddFunction(spv::OpBranch, merge); + EmitLabel(state, merge); + std::array values{}; + for (std::uint32_t dword = 0; dword < (bits == 32u ? dwords : 1u); ++dword) { + if ((used & (1u << dword)) == 0u) { + values[dword] = ConstantU32(state, 0u); + continue; + } + values[dword] = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, u32, values[dword], directValues[dword], directExit, lookupValues[dword], lookupExit); + } + return values; +} + std::uint32_t EmitBdaRead(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t bits) { auto& state = ctx.state; if (bits != 8u && bits != 16u && bits != 32u) ctx.Fail(inst, "unsupported BDA read width"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index c454eee2c5..e8c470ddaa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -203,6 +203,28 @@ std::uint32_t GuestAddress(SpirvValueEmitContext& ctx, const IrValue& inst, cons return AddBdaImmediate(ctx, inst, address, immediate); } +std::uint32_t GuestAddressLow(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { + const auto low = ctx.Arg(inst, 1); + if (mem.kind != ResourceKind::ScalarAddress) return low; + return Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), low, ConstantU32(ctx.state, ~3u)); +} + +std::array ReadBdaDwords(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t dwords, std::uint32_t used) { + const auto lookup = [&] { return EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, dwords, false, used); }; + const auto* anchor = FindBdaAnchor(ctx.state, inst); + if (anchor == nullptr) return lookup(); + return EmitAnchoredBdaRead(ctx.state, inst, *anchor, GuestAddressLow(ctx, inst, mem), static_cast(mem.offset), 32u, dwords, used, lookup); +} + +std::uint32_t ReadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t bits) { + const auto lookup = [&] { return std::array{EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits)}; }; + const auto* anchor = FindBdaAnchor(ctx.state, inst); + if (anchor == nullptr) return lookup()[0]; + auto immediate = mem.offset; + if (mem.kind == ResourceKind::ScalarAddress) immediate &= ~3u; + return EmitAnchoredBdaRead(ctx.state, inst, *anchor, GuestAddressLow(ctx, inst, mem), static_cast(immediate), bits, 1u, 1u, lookup)[0]; +} + bool SameAddressValue(const IrValue* left, const IrValue* right, std::uint32_t depth = 0u) { if (left == right) return true; if (left == nullptr || right == nullptr || depth > 3u || left->Opcode() != right->Opcode() || left->ArgumentCount() != right->ArgumentCount()) return false; @@ -272,7 +294,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem const auto none = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vector, none, zero, zero, zero, zero); const auto all = EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), vector, none, [&]() { - const auto values = EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, 4u, false, used); + const auto values = ReadBdaDwords(ctx, inst, mem, 4u, used); const auto read = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, vector, read, values[0], values[1], values[2], values[3]); return read; @@ -284,7 +306,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem } } return EmitValueOrZeroIfCondition(ctx.state, ActiveArgument(ctx, inst), [&]() { - return EmitBdaRead(ctx, inst, GuestAddress(ctx, inst, mem), bits); + return ReadBda(ctx, inst, mem, bits); }); } @@ -578,7 +600,7 @@ void LoadAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t std::uint32_t LoadBdaWide(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components) { auto& state = ctx.state; return EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), TypeU32Composite(state, components), ConstantU32CompositeZero(state, components), [&]() { - return ConstructU32Composite(state, components, EmitBdaDwordReads(ctx, inst, GuestAddressBase(ctx, inst, mem), mem.offset, components)); + return ConstructU32Composite(state, components, ReadBdaDwords(ctx, inst, mem, components, 0u)); }); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp index 395e365f6e..d2a17cc285 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvModuleEmitter.cpp @@ -1164,6 +1164,7 @@ void EmitProgram(SpirvEmitterState& state) { state.module.AddFunction(spv::OpStore, state.pixelValidMaskVariable, ConstantU32(state, 1u)); } if (AlphaToCoverage(state)) StoreCoverage(state, ConstantI32(state, -1)); + EmitBdaAnchors(state); if (OrderedPixelShader(state)) { state.module.AddFunction(spv::OpBeginInvocationInterlockEXT); } From 3265cb6d79969021002c1808efee4d220668a74d Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Wed, 14 Oct 2026 10:09:08 -0400 Subject: [PATCH 002/131] fix(shader): keep BDA anchors exact on tables whose ranges overlap The driver builds tables whose ranges ascend by begin but can overlap (a per-build region read over the address space's ranges). A lookup selects the last range beginning at or below an address, so the anchor's range now ends at the next range's begin when that comes first, and the driver requires only ascending begins (BdaAbi::RangesOrdered). An anchor is also not used for a range longer than 2^62 bytes, whose offset arithmetic could wrap with a positive immediate. Anchors are counted per read site after adjacent loads are grouped and reads with no used dwords are dropped, so a program does not search for one effective read; read eligibility shares RoutesApertures. Tests: the anchored test programs can replace the lookup path by a sentinel, which shows which reads load directly; the anchor table has an overlapping range; a recompiled compute program reads through an anchor on the GPU. AI-assisted: yes --- .../Graphics/src/BdaResources.cpp | 2 +- .../libSceAgcDriver/tests/BdaContracts.cpp | 5 +- .../libSceAgcDriver/tests/BdaExecution.cpp | 57 +++++- .../prx/libSceAgcDriver/tests/BdaShader.cpp | 4 + .../prx/libSceAgcDriver/tests/BdaShader.hpp | 2 + .../libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- .../tests/execution/BdaAnchorReads.cpp | 164 ++++++++++++++++++ core/shader/recompiler/BdaAbi.hpp | 5 +- .../include/SpirvBackend/SpirvBda.hpp | 3 + .../SpirvBackend/src/SpirvBdaLookup.cpp | 5 +- .../SpirvBackend/src/SpirvBdaRead.cpp | 62 +++---- .../SpirvMemory/SpirvMemoryInstructions.cpp | 37 +++- 12 files changed, 295 insertions(+), 53 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp index 4bb8b097ed..eb3c163130 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BdaResources.cpp @@ -192,7 +192,7 @@ BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memo ++it; } } - Require(ShaderRecompiler::BdaAbi::RangesOrdered(ranges.data(), ranges.size()), "BDA table ranges are not ascending and disjoint"); + Require(ShaderRecompiler::BdaAbi::RangesOrdered(ranges.data(), ranges.size()), "BDA table ranges do not ascend by begin"); for (const auto& range : ranges) Require((range.permissions & ShaderRecompiler::BdaAbi::Write) == 0 || range.end <= ShaderRecompiler::BdaAbi::WrittenAddressLimit, "a writable BDA range ends past the addresses the written-page record can hold"); table = std::make_shared(context, tableBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); const ShaderRecompiler::BdaAbi::Header header{ShaderRecompiler::BdaAbi::Version, static_cast(ranges.size()), sizeof(ShaderRecompiler::BdaAbi::Range), 0}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp index dd65417557..e113105055 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaContracts.cpp @@ -58,8 +58,7 @@ void RunBdaContractTests() { reject([&] { static_cast(serializer.Deserialize(invalid)); }, "request signature"); reject([&] { static_cast(serializer.Deserialize("NVNQQWMAAAA=")); }, "serialization version"); const auto ordered = [](std::initializer_list ranges) { return BdaAbi::RangesOrdered(ranges.begin(), ranges.size()); }; - AgcDriver::Graphics::Require(ordered({}) && ordered({{0x1000, 0x2000, 1, 1, 0}, {0x2000, 0x2000, 1, 1, 0}, {0x2000, 0x3000, 1, 1, 0}, {0x4000, 0x5000, 1, 1, 0}}), "ascending disjoint BDA ranges were refused"); - AgcDriver::Graphics::Require(!ordered({{0x1000, 0x3000, 1, 1, 0}, {0x2000, 0x2100, 1, 1, 0}}), "overlapping BDA ranges were accepted"); + AgcDriver::Graphics::Require(ordered({}) && ordered({{0x1000, 0x2000, 1, 1, 0}, {0x2000, 0x2000, 1, 1, 0}, {0x2000, 0x3000, 1, 1, 0}, {0x4000, 0x5000, 1, 1, 0}}), "ascending BDA ranges were refused"); + AgcDriver::Graphics::Require(ordered({{0x1000, 0x3000, 1, 1, 0}, {0x2f00, 0x3100, 1, 1, 0}}), "BDA ranges ascending by begin and overlapping were refused"); AgcDriver::Graphics::Require(!ordered({{0x4000, 0x5000, 1, 1, 0}, {0x1000, 0x2000, 1, 1, 0}}), "unsorted BDA ranges were accepted"); - AgcDriver::Graphics::Require(!ordered({{0x2000, 0x1000, 1, 1, 0}}), "a BDA range ending before its begin was accepted"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index e3a7a3f99d..113991ac43 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -228,22 +228,24 @@ static void RunBdaAnchorTests(const Context& context) { Buffer large(context, 1024, usage); Buffer top(context, 16, usage); Buffer above(context, 16, usage); + Buffer overlap(context, 0x200, usage); std::uint32_t seed = 0x11; - for (auto* buffer : {&first, &second, &words, &shifted, &large, &top, &above}) { + for (auto* buffer : {&first, &second, &words, &shifted, &large, &top, &above, &overlap}) { for (std::size_t index = 0; index < buffer->Bytes().size(); ++index) buffer->Bytes()[index] = std::byte(static_cast(seed + index * 7u)); seed += 0x33; } for (std::uint32_t index = 0; index < 16u; ++index) words.Bytes()[index] = std::byte(static_cast(0xa0u + index)); for (std::uint32_t index = 0; index < 20u; ++index) shifted.Bytes()[index] = std::byte(static_cast(0xc0u + index)); - const std::array ranges{{{guest, guest + 3, first.DeviceAddress(), Abi::Read, 0}, + std::vector ranges{{guest, guest + 3, first.DeviceAddress(), Abi::Read, 0}, {guest + 3, guest + 5, second.DeviceAddress(), Abi::Read, 0}, {guest + 0x100, guest + 0x110, words.DeviceAddress(), Abi::Read, 0}, {guest + 0x200, guest + 0x210, shifted.DeviceAddress() + 1u, Abi::Read, 0}, {guest + 0x400, guest + 0x800, large.DeviceAddress(), Abi::Read, 0}, + {guest + 0x700, guest + 0x900, overlap.DeviceAddress(), Abi::Read, 0}, {limit - 0x10, limit, top.DeviceAddress(), Abi::Read, 0}, - {highest, highest + 0x10, above.DeviceAddress(), Abi::Read, 0}}}; - Require(Abi::RangesOrdered(ranges.data(), ranges.size()), "the BDA anchor test table is not ascending and disjoint"); - Buffer table(context, sizeof(Abi::Header) + sizeof(ranges), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); + {highest, highest + 0x10, above.DeviceAddress(), Abi::Read, 0}}; + Require(Abi::RangesOrdered(ranges.data(), ranges.size()), "the BDA anchor test table does not ascend by begin"); + Buffer table(context, sizeof(Abi::Header) + ranges.size() * sizeof(Abi::Range), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); Buffer fault(context, sizeof(Abi::Fault), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); Buffer output(context, (BdaAnchoredReadInputWord + 3u + lanes) * sizeof(std::uint32_t), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT); struct Outcome { @@ -254,10 +256,10 @@ static void RunBdaAnchorTests(const Context& context) { std::uint32_t permissions = Abi::Read; const auto run = [&](Pipeline& pipeline, std::uint32_t read, std::uint64_t anchor, const std::vector& lows) { auto copy = ranges; - copy[2].permissions = permissions; + if (copy.size() > 2u) copy[2].permissions = permissions; const Abi::Header header{Abi::Version, count, sizeof(Abi::Range), 0}; std::memcpy(table.Bytes().data(), &header, sizeof(header)); - std::memcpy(table.Bytes().data() + sizeof(header), copy.data(), sizeof(copy)); + std::memcpy(table.Bytes().data() + sizeof(header), copy.data(), copy.size() * sizeof(Abi::Range)); std::memset(fault.Bytes().data(), 0, fault.Bytes().size()); std::memset(output.Bytes().data(), 0xa5, BdaAnchoredReadInputWord * sizeof(std::uint32_t)); std::vector inputs{static_cast(anchor), static_cast(anchor >> 32u), read}; @@ -300,7 +302,7 @@ static void RunBdaAnchorTests(const Context& context) { for (const std::int32_t immediate : {0, 4, -1, -8, 0x7ffffff0, std::numeric_limits::min()}) { for (const auto& [bits, dwords, used] : std::initializer_list>{{8, 1, 1}, {16, 1, 1}, {32, 1, 1}, {32, 4, 15}, {32, 4, 10}, {32, 3, 4}}) reads.push_back({immediate, bits, dwords, used}); } - const auto anchors = {guest + 0x100, guest + 0x108, guest, guest + 3, guest + 0x200, guest - 1, guest + 0x110, guest + 0x400, limit - 0x10, limit - 4, highest, highest + 8, std::numeric_limits::max() - 2}; + const auto anchors = {guest + 0x100, guest + 0x108, guest, guest + 3, guest + 0x200, guest - 1, guest + 0x110, guest + 0x400, guest + 0x6f0, guest + 0x700, guest + 0x800, limit - 0x10, limit - 4, highest, highest + 8, std::numeric_limits::max() - 2}; std::vector lows{0u, 1u, 2u, 3u, 4u, 8u, 0xcu, 0xdu, 0xeu, 0xfu, 0x10u, 0x14u, 0x100u, 0x108u, 0x10cu, 0x1fcu, 0x200u, 0x20eu, 0x3f0u, 0x3fcu, 0x400u, 0x7fffu, 0x7fffffffu, 0x80000000u, 0xfffffff0u, 0xfffffffcu, 0xffffffffu}; for (std::uint32_t lane = 0; lane < 32u; ++lane) lows.push_back(lane * 0x24u); std::vector strided; @@ -316,6 +318,39 @@ static void RunBdaAnchorTests(const Context& context) { auto stopping = build({reads, true, true, false, 1}); compare(*stopping, anchors, {0u, 0x80000000u, 0xfffffffcu}); const auto check = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { return run(stopping->anchored, read, anchor, {low}); }; + auto direct = std::make_unique(context, MakeBdaAnchoredReadTestShader({reads, true, false, false, 1, true}, true), std::array{&table, &fault, &output}); + const auto dword = [](Buffer& buffer, std::size_t offset) { + std::uint32_t value = 0; + std::memcpy(&value, buffer.Bytes().data() + offset, sizeof(value)); + return value; + }; + const auto loads = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low, std::initializer_list values) { + const auto outcome = run(*direct, read, anchor, {low}); + Require(outcome.fault.state == Abi::FaultState::Empty && std::equal(values.begin(), values.end(), outcome.values.begin()), "a read the anchor holds did not load directly at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); + }; + const auto misses = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) { + const auto outcome = run(*direct, read, anchor, {low}); + Require(outcome.fault.state == Abi::FaultState::Empty && outcome.values[0] == BdaAnchoredReadSentinel, "a read the anchor does not hold did not take the lookup path at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read)); + }; + loads(2, guest + 0x100, 0, {dword(words, 0)}); + loads(2, guest + 0x100, 0xc, {dword(words, 0xc)}); + loads(3, guest + 0x100, 0, {dword(words, 0), dword(words, 4), dword(words, 8), dword(words, 0xc)}); + loads(0, guest + 0x200, 0xd, {0xceu}); + loads(0, guest + 3, 1, {static_cast(second.Bytes()[1])}); + loads(1, guest, 1, {static_cast(first.Bytes()[1]) | static_cast(first.Bytes()[2]) << 8u}); + loads(2, guest + 0x400, 0x2fc, {dword(large, 0x2fc)}); + loads(2, guest + 0x700, 0x1fc, {dword(overlap, 0x1fc)}); + loads(2, limit - 0x10, 0xc, {dword(top, 0xc)}); + misses(2, guest + 0x100, 0xd); + misses(3, guest + 0x100, 4); + misses(2, guest + 0x200, 0); + misses(1, guest + 2, 0); + misses(0, guest + 3, 2); + misses(2, guest + 0x400, 0x300); + misses(2, guest + 0x400, 0x2fd); + misses(2, limit - 0x10, 0xd); + misses(2, highest, 0); + misses(2, guest - 1, 1); const auto wide = check(3, guest + 0x100, 0); Require(wide.fault.state == Abi::FaultState::Empty && wide.values[0] == 0xa3a2a1a0u && wide.values[1] == 0xa7a6a5a4u && wide.values[2] == 0xabaaa9a8u && wide.values[3] == 0xafaeadacu, "an anchored span read returned wrong data"); const auto some = check(4, guest + 0x100, 0); @@ -329,12 +364,18 @@ static void RunBdaAnchorTests(const Context& context) { const auto unmapped = check(2, guest + 0x100, 0x10); Require(unmapped.fault.state == Abi::FaultState::Ready && unmapped.fault.reason == Abi::FaultReason::Unmapped && unmapped.fault.address == guest + 0x110 && unmapped.fault.instruction == 0x1234 && unmapped.values[0] == 0xa5a5a5a5u, "an anchored read past its range did not fault"); permissions = 0; + misses(2, guest + 0x100, 0); compare(*stopping, {guest + 0x100}, {0u, 4u}); compare(*elected, {guest + 0x100}, lows); permissions = Abi::Read; count = 3; + misses(2, guest + 0x100, 0); + misses(0, guest, 0); compare(*stopping, {guest + 0x100, guest}, {0u, 4u}); compare(*elected, {guest + 0x100, guest}, lows); + ranges = {{0, limit, large.DeviceAddress(), Abi::Read, 0}}; + count = 1; + compare(*stopping, {limit - 1}, {0xffffffffu}); } void RunBdaExecutionTests(const Context& context) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index a24dd6a955..77707e646e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -204,6 +204,10 @@ std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadPr } EmitIfCondition(state, Binary(state, spv::OpIEqual, TypeBool(state), selector, ConstantU32(state, index)), [&] { const auto lookup = [&] { + if (reads.sentinel) { + const auto sentinel = ConstantU32(state, BdaAnchoredReadSentinel); + return std::array{sentinel, sentinel, sentinel, sentinel}; + } const auto address = AddBdaAddress(ctx, instruction, base, Unary(state, spv::OpUConvert, u64, low), false); if (read.bits == 32u) return EmitBdaDwordReads(ctx, instruction, address, static_cast(read.immediate), read.dwords); return std::array{EmitBdaRead(ctx, instruction, AddBdaImmediate(ctx, instruction, address, read.immediate), read.bits)}; diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index ec04f67aba..555397c843 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -24,7 +24,9 @@ struct BdaAnchoredReadProgram { bool elect = false; bool coherent = false; std::uint32_t lanes = 1; + bool sentinel = false; }; +inline constexpr std::uint32_t BdaAnchoredReadSentinel = 0xdeadbeefu; std::vector MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored); void RunBdaStoreExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 9cded9c715..53f3512c90 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3703,7 +3703,7 @@ void verifyBdaAnchors() { }; for (const std::uint32_t stride : {4u, 32u}) { for (const std::uint32_t loads : {0u, 1u, 2u, 4u}) { - const bool anchored = loads >= 2u; + const bool anchored = stride == 32u && loads >= 2u; const auto words = compile(loads, stride); std::map names; std::string function; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp new file mode 100644 index 0000000000..9bd219d267 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BdaAnchorReads.cpp @@ -0,0 +1,164 @@ +#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 = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t InputDwords = 1024; +constexpr std::uint32_t Fill = 0xcdcdcdcdu; +constexpr std::uint32_t OutputBase = 0x8000; +constexpr std::uint32_t OutputDwords = 8; + +alignas(256) constexpr std::array Code{ + 0x34040082, 0x34060085, 0x4a0606ff, OutputBase, + 0xdc308010, 0x04000002, 0xdc308014, 0x05000002, 0xdc308018, 0x06000002, + 0xdc308403, 0x07000002, + 0xf4000100, 0xfa000203, 0xf4000140, 0xfa0002f6, + 0xbf8c3f70, 0xbf8cc07f, 0x7e100204, 0x7e120205, + 0xdc708000, 0x00000403, 0xdc708004, 0x00000503, 0xdc708008, 0x00000603, 0xdc70800c, 0x00000703, + 0xdc708010, 0x00000803, 0xdc708014, 0x00000903, 0xbf810000}; + +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, "BDA anchor reads: cannot allocate the guest block"); + 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; + + std::uint8_t* Data() { return 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; +} + +std::vector Initial() { + std::vector memory(BlockBytes / 4u, Fill); + for (std::uint32_t dword = 0; dword < InputDwords; ++dword) memory[dword] = 0x61000000u + dword * 0x00010307u; + return memory; +} + +std::uint32_t Bytes(const std::vector& memory, std::uint32_t address) { + std::uint32_t value = 0; + for (std::uint32_t byte = 0; byte < 4u; ++byte) { + const std::uint32_t at = address + byte; + value |= ((memory[at / 4u] >> (at % 4u * 8u)) & 0xffu) << (byte * 8u); + } + return value; +} + +std::vector Expected() { + const auto initial = Initial(); + auto memory = initial; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* out = &memory[OutputBase / 4u + tid * OutputDwords]; + out[0] = Bytes(initial, tid * 4u + 0x10u); + out[1] = Bytes(initial, tid * 4u + 0x14u); + out[2] = Bytes(initial, tid * 4u + 0x18u); + out[3] = Bytes(initial, tid * 4u + 0x403u); + out[4] = Bytes(initial, 0x200u); + out[5] = Bytes(initial, 0x2f4u); + } + return memory; +} + +ShaderRecompiler::RecompileResult Recompile(std::uint32_t waveSize, std::span userData, std::span guest, const ShaderRecompiler::SpirvTarget& target) { + const std::span code(Code); + const std::array regions{{{reinterpret_cast(code.data()), std::as_bytes(code)}, {reinterpret_cast(guest.data()), guest}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, regions}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +bool SearchesAnchor(const ShaderRecompiler::SharedSpirv& spirv) { + const std::string_view text(reinterpret_cast(spirv.data()), spirv.size() * sizeof(std::uint32_t)); + return text.find("bda_anchor_range") != std::string_view::npos; +} + +void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target, const std::string& name) { + auto memory = Initial(); + std::memcpy(guest.Data(), memory.data(), BlockBytes); + const auto address = static_cast(reinterpret_cast(guest.Data())); + const std::vector userData{static_cast(address), static_cast(address >> 32u)}; + const auto result = Recompile(waveSize, userData, std::span(reinterpret_cast(guest.Data()), InputDwords * sizeof(std::uint32_t)), target); + Require(SearchesAnchor(result.spirv), "BDA anchor reads: " + name + " reads of one user-data base do not share an anchor"); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(Code.data())); + device.WaitIdle(); + std::memcpy(memory.data(), guest.Data(), BlockBytes); + const auto expected = Expected(); + for (std::size_t dword = 0; dword < memory.size(); ++dword) { + Require(memory[dword] == expected[dword], "BDA anchor reads: " + name + " byte " + std::to_string(dword * 4u) + " is " + Hex(memory[dword]) + ", expected " + Hex(expected[dword])); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + GuestBlock guest; + const auto native = device->Target(); + const auto split = device->ComputeTarget(32); + Run(*device, guest, 32, native, "wave32"); + Run(*device, guest, 64, native, "wave64"); + if (split.subgroupSize != native.subgroupSize) Run(*device, guest, 64, split, "wave64 split"); + std::puts("BDA anchor read tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/BdaAbi.hpp b/core/shader/recompiler/BdaAbi.hpp index bdb3049862..48d8d2bce1 100644 --- a/core/shader/recompiler/BdaAbi.hpp +++ b/core/shader/recompiler/BdaAbi.hpp @@ -28,9 +28,8 @@ struct Range { }; inline bool RangesOrdered(const Range* ranges, std::size_t count) { - for (std::size_t index = 0; index < count; ++index) { - if (ranges[index].end < ranges[index].begin) return false; - if (index != 0 && ranges[index].begin < ranges[index - 1].end) return false; + for (std::size_t index = 1; index < count; ++index) { + if (ranges[index].begin < ranges[index - 1].begin) return false; } return true; } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index 67b486dd52..1d280f1017 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -38,6 +38,9 @@ std::uint32_t EmitBdaAtomic(SpirvValueEmitContext& ctx, const IrValue& inst, std std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, std::uint32_t dwords, bool everyDword = false, std::uint32_t usedDwords = 0u); bool BdaByteReadsForced(); std::uint32_t AddBdaAddress(SpirvValueEmitContext& ctx, const IrValue& inst, std::uint32_t address, std::uint32_t offset, bool subtract); +bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem); +std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords); +std::vector BdaReadSites(const SpirvEmitterState& state); void CollectBdaAnchors(SpirvEmitterState& state); void DefineBdaAnchorFunction(SpirvEmitterState& state); void EmitBdaAnchors(SpirvEmitterState& state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index 55172995b1..c87aa246a2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -128,8 +128,11 @@ std::uint32_t DefineBdaLookup(SpirvEmitterState& state, const char* name, bool r {binary(spv::OpULessThanEqual, boolean, deviceEnd, result), BdaAbi::FaultReason::Overflow}, }); if (rangeResult) { + const auto last = binary(spv::OpUGreaterThanEqual, boolean, index, count); + const auto next = BdaLoadAddress(state, binary(spv::OpIAdd, u32, constant(4), binary(spv::OpIMul, u32, Select(state, u32, last, binary(spv::OpISub, u32, index, constant(1)), index), constant(8)))); + const auto bound = Select(state, u64, binary(spv::OpLogicalOr, boolean, last, binary(spv::OpULessThan, boolean, finish, next)), finish, next); const auto range = state.module.AllocateId(); - state.module.AddFunction(spv::OpCompositeConstruct, rangeType, range, begin, finish, base); + state.module.AddFunction(spv::OpCompositeConstruct, rangeType, range, begin, bound, base); state.module.AddFunction(spv::OpReturnValue, range); state.module.AddFunction(spv::OpFunctionEnd); return function; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 73a196bfc6..2237fa9714 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -105,23 +105,6 @@ std::uint32_t EmitBdaBytes(SpirvEmitterState& state, std::uint32_t address, std: return EmitBdaByteLoop(state, address, ConstantU32(state, bytes), instruction, accessMask); } -// The dwords of a wide load that the program extracts (all of them when the composite is used -// whole). Dead-code elimination has dropped the unused extracts, as it dropped unused dword loads -// when every dword was its own instruction. -std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords) { - const auto all = (1u << dwords) - 1u; - if (dwords == 1u) return all; - std::uint32_t used = 0; - for (const IrValue* user : inst.Uses()) { - const auto opcode = user->Opcode(); - if (opcode != IrOpcode::CompositeExtractU32x2 && opcode != IrOpcode::CompositeExtractU32x3 && opcode != IrOpcode::CompositeExtractU32x4) return all; - const IrValue* index = user->ArgumentCount() > 1u ? user->Argument(1) : nullptr; - if (index == nullptr || !index->HasImmediate() || index->ImmediateU32() >= dwords) return all; - used |= 1u << index->ImmediateU32(); - } - return used; -} - void EmitBdaStoreAt(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t pointerType, std::uint32_t value, std::uint32_t instruction, std::uint32_t accessMask) { const auto physical = state.module.AllocateId(); state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaWritePointerFunction, address, ConstantU32(state, bytes), instruction); @@ -379,6 +362,24 @@ bool BdaByteReadsForced() { return forced; } +std::uint32_t UsedBdaDwords(const IrValue& inst, std::uint32_t dwords) { + const auto all = (1u << dwords) - 1u; + if (dwords == 1u) return all; + std::uint32_t used = 0; + for (const IrValue* user : inst.Uses()) { + const auto opcode = user->Opcode(); + if (opcode != IrOpcode::CompositeExtractU32x2 && opcode != IrOpcode::CompositeExtractU32x3 && opcode != IrOpcode::CompositeExtractU32x4) return all; + const IrValue* index = user->ArgumentCount() > 1u ? user->Argument(1) : nullptr; + if (index == nullptr || !index->HasImmediate() || index->ImmediateU32() >= dwords) return all; + used |= 1u << index->ImmediateU32(); + } + return used; +} + +bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem) { + return mem.kind == ResourceKind::Flat && state.program.Resources().stage == IrShaderStage::Compute; +} + std::uint32_t BdaInstructionPc(SpirvEmitterState& state, const IrValue& inst) { return state.bdaPcOverride != 0u ? state.bdaPcOverride : ConstantU32(state, inst.Flags().pc); } @@ -425,15 +426,16 @@ namespace { constexpr std::size_t MaxBdaAnchors = 4; constexpr std::uint64_t BdaAnchorAddressLimit = ~std::uint64_t{0} << 32u; +constexpr std::uint64_t BdaAnchorSpanLimit = std::uint64_t{1} << 62u; -std::optional> UserDataBase(const IrProgram& program, const IrValue& inst) { +std::optional> UserDataBase(const SpirvEmitterState& state, const IrValue& inst) { if (AddressOpcodeInfoOf(inst.Opcode()).access != AddressAccess::Read || inst.ArgumentCount() < 2u) return std::nullopt; const auto index = inst.Flags().index; - const auto& memory = program.Resources().memoryInfo; + const auto& memory = state.program.Resources().memoryInfo; if (index >= memory.size()) return std::nullopt; const auto& info = memory[index]; if (info.kind != ResourceKind::ScalarAddress && info.kind != ResourceKind::Flat && info.kind != ResourceKind::Global) return std::nullopt; - if (info.planningOnly || info.addressIsFull || (info.kind == ResourceKind::Flat && program.Resources().stage == IrShaderStage::Compute)) return std::nullopt; + if (info.planningOnly || info.addressIsFull || RoutesApertures(state, info)) return std::nullopt; const IrValue* handle = inst.Argument(0) != nullptr ? inst.Argument(0)->Resolve() : nullptr; if (handle == nullptr || handle->Opcode() != IrOpcode::GetAddressResource || handle->ArgumentCount() != 2u) return std::nullopt; const auto userData = [](const IrValue* value) -> std::optional { @@ -495,14 +497,12 @@ void CollectBdaAnchors(SpirvEmitterState& state) { state.bdaAnchors.clear(); if (!state.program.Info().usesDma || state.bdaProbeFunction == 0) return; std::vector, std::size_t>> bases; - for (const IrBlock* block : state.program.BlockOrder()) { - for (const IrValue* inst : block->Instructions()) { - const auto base = UserDataBase(state.program, *inst); - if (!base) continue; - const auto found = std::find_if(bases.begin(), bases.end(), [&](const auto& entry) { return entry.first == *base; }); - if (found != bases.end()) ++found->second; - else bases.push_back({*base, 1u}); - } + for (const IrValue* inst : BdaReadSites(state)) { + const auto base = UserDataBase(state, *inst); + if (!base) continue; + const auto found = std::find_if(bases.begin(), bases.end(), [&](const auto& entry) { return entry.first == *base; }); + if (found != bases.end()) ++found->second; + else bases.push_back({*base, 1u}); } std::stable_sort(bases.begin(), bases.end(), [](const auto& left, const auto& right) { return left.second > right.second; }); for (const auto& [base, reads] : bases) { @@ -536,6 +536,7 @@ void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& const auto deviceEnd = Binary(state, spv::OpIAdd, u64, anchor.device, span); auto usable = Binary(state, spv::OpLogicalAnd, boolean, Binary(state, spv::OpINotEqual, boolean, anchor.device, BdaConstant(state, 0u)), Binary(state, spv::OpUGreaterThan, boolean, deviceEnd, anchor.device)); usable = Binary(state, spv::OpLogicalAnd, boolean, usable, Binary(state, spv::OpULessThanEqual, boolean, end, BdaConstant(state, BdaAnchorAddressLimit))); + usable = Binary(state, spv::OpLogicalAnd, boolean, usable, Binary(state, spv::OpULessThanEqual, boolean, span, BdaConstant(state, BdaAnchorSpanLimit))); anchor.size = Select(state, u64, usable, span, BdaConstant(state, 0u)); } @@ -550,10 +551,11 @@ void EmitBdaAnchors(SpirvEmitterState& state) { const SpirvEmitterState::BdaAnchor* FindBdaAnchor(const SpirvEmitterState& state, const IrValue& inst) { if (state.bdaAnchors.empty()) return nullptr; - const auto base = UserDataBase(state.program, inst); + const auto base = UserDataBase(state, inst); if (!base) return nullptr; for (const auto& anchor : state.bdaAnchors) { - if (anchor.lowRegister == base->first && anchor.highRegister == base->second && anchor.size != 0u) return &anchor; + const bool emitted = anchor.size != 0u; + if (emitted && anchor.lowRegister == base->first && anchor.highRegister == base->second) return &anchor; } return nullptr; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index e8c470ddaa..6e2593fd1e 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace ShaderRecompiler { @@ -247,10 +248,10 @@ bool WritesMemory(IrOpcode opcode) { image == ImageAccess::Write || image == ImageAccess::Atomic || opcode == IrOpcode::Barrier || opcode == IrOpcode::StoreVectorWindow; } -std::vector> AdjacentBdaLoads(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem) { +std::vector> AdjacentBdaLoads(const SpirvEmitterState& state, const IrValue& inst, const MemoryInfo& mem) { std::vector> members; const IrBlock* block = inst.Parent(); - if (block == nullptr || ctx.state.laneCount != 1u || (mem.offset & 3u) != 0u) return members; + if (block == nullptr || state.laneCount != 1u || (mem.offset & 3u) != 0u) return members; const auto& instructions = block->Instructions(); auto next = std::find(instructions.begin(), instructions.end(), &inst); if (next == instructions.end()) return members; @@ -261,7 +262,7 @@ std::vector> AdjacentBdaLoads(SpirvValu if (WritesMemory(other->Opcode())) break; continue; } - const auto& otherMemory = ctx.Memory(*other); + const auto& otherMemory = state.program.Resources().memoryInfo.at(other->Flags().index); if (otherMemory.planningOnly || otherMemory.kind != mem.kind || otherMemory.addressIsFull != mem.addressIsFull || otherMemory.coherent != mem.coherent || other->ArgumentCount() != inst.ArgumentCount()) continue; bool sameArguments = true; for (std::size_t index = 0; index < inst.ArgumentCount(); ++index) { @@ -285,7 +286,7 @@ std::uint32_t LoadBda(SpirvValueEmitContext& ctx, const IrValue& inst, const Mem state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, grouped->second.first, grouped->second.second); return value; } - const auto members = AdjacentBdaLoads(ctx, inst, mem); + const auto members = AdjacentBdaLoads(state, inst, mem); if (!members.empty()) { std::uint32_t used = 1u; for (const auto& member : members) used |= 1u << member.second; @@ -438,10 +439,34 @@ const MemoryInfo& SharedMemory(SpirvValueEmitContext& ctx, const IrValue& inst) return mem; } -bool RoutesApertures(const SpirvEmitterState& state, const MemoryInfo& mem) { - return mem.kind == ResourceKind::Flat && state.program.Resources().stage == IrShaderStage::Compute; + } +std::vector BdaReadSites(const SpirvEmitterState& state) { + std::vector sites; + std::unordered_set grouped; + const auto& memory = state.program.Resources().memoryInfo; + for (const IrBlock* block : state.program.BlockOrder()) { + for (const IrValue* inst : block->Instructions()) { + const auto info = AddressOpcodeInfoOf(inst->Opcode()); + const auto index = inst->Flags().index; + if (info.access != AddressAccess::Read || index >= memory.size() || grouped.contains(inst)) continue; + const auto& mem = memory[index]; + if (mem.planningOnly || RoutesApertures(state, mem)) continue; + if (mem.kind != ResourceKind::ScalarAddress && mem.kind != ResourceKind::Flat && mem.kind != ResourceKind::Global) continue; + if (info.dataBits == 32u && info.components == 1u) { + for (const auto& member : AdjacentBdaLoads(state, *inst, mem)) grouped.insert(member.first); + } else if (info.components > 1u && UsedBdaDwords(*inst, info.components) == 0u) { + continue; + } + sites.push_back(inst); + } + } + return sites; +} + +namespace { + struct ApertureAddress { std::uint32_t low = 0; std::uint32_t shared = 0; From faef9e3b87e301d052792b2c5086989d50de437b Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:02:08 +0200 Subject: [PATCH 003/131] fix(libSceNet): guard recvmsg guest outputs --- core/libs/prx/libSceNet/CMakeLists.txt | 2 +- core/libs/prx/libSceNet/Export.cpp | 9 ++++ core/libs/tests/GuestSocketHostWrite.cpp | 64 +++++++++++++++++++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 98776d4b5e..4bd77b100e 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -20,7 +20,7 @@ if(BUILD_TESTING) target_include_directories(guest_socket_host_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_socket_host_write_tests PRIVATE libSceNet libkernel libc) configure_windows_unwind(guest_socket_host_write_tests) - foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom) + foreach(api IN ITEMS recv recvfrom read sceKernelRead sceNetRecv sceNetRecvfrom sceNetRecvmsg) add_test(NAME guest_socket_host_write_${api}_udp COMMAND guest_socket_host_write_tests ${api}) add_test(NAME guest_socket_host_write_${api}_tcp COMMAND guest_socket_host_write_tests ${api} tcp) set_tests_properties(guest_socket_host_write_${api}_udp guest_socket_host_write_${api}_tcp diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index e03f88e831..b675ff29c1 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -953,6 +954,14 @@ int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } + const GuestArena::HostWrite messageOutput(msg, sizeof(*msg)); + if (!messageOutput.Open()) return fail(NET_EFAULT); + std::deque destinations; + for (int i = 0; i < msg->iov_length; ++i) { + const auto& entry = msg->iov[i]; + if (entry.length == 0) continue; + if (!destinations.emplace_back(entry.base, static_cast(entry.length)).Open()) return fail(NET_EFAULT); + } const bool datagram = socket.type != NET_SOCK_STREAM; std::vector buffer; try { diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index 06e23b986e..c6a09d49d6 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -5,12 +5,29 @@ #include #include #include +#include #include #include #include #include #include +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t nameLength; + NetIovec* iov; + int iovLength; + void* control; + std::uint32_t controlLength; + int flags; +}; +static_assert(sizeof(NetMsghdr) == 48 && offsetof(NetMsghdr, iov) == 16 && offsetof(NetMsghdr, flags) == 44); + extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); @@ -36,6 +53,7 @@ int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); int APS5_VABI sceNetSocketClose(int); int* APS5_VABI sceNetErrnoLoc(); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); @@ -61,7 +79,7 @@ static DWORD Protection(const void* address) { int main(int argc, char** argv) { constexpr std::size_t page = 0x4000; constexpr std::size_t capacity = 32; - constexpr const char* names[] = {"recv", "recvfrom", "read", "sceKernelRead", "sceNetRecv", "sceNetRecvfrom"}; + constexpr const char* names[] = {"recv", "recvfrom", "read", "sceKernelRead", "sceNetRecv", "sceNetRecvfrom", "sceNetRecvmsg"}; Require(argc == 2 || argc == 3, "receive API argument required"); Require(argc != 3 || std::strcmp(argv[2], "tcp") == 0, "unknown transport"); const bool stream = argc == 3; @@ -121,7 +139,13 @@ int main(int argc, char** argv) { case 2: return read_nid_postfix(receiver, buffer, bytes); case 3: return sceKernelRead(receiver, buffer, bytes); case 4: return sceNetRecv(receiver, buffer, bytes, flags); - default: return sceNetRecvfrom(receiver, buffer, bytes, flags, address, addressLength); + case 5: return sceNetRecvfrom(receiver, buffer, bytes, flags, address, addressLength); + default: { + const auto first = std::min(bytes, 3); + NetIovec vectors[]{{buffer, first}, {nullptr, 0}, {static_cast(buffer) + first, bytes - first}}; + NetMsghdr message{nullptr, 0, vectors, 3, nullptr, 0, 0}; + return sceNetRecvmsg(receiver, &message, flags); + } } }; const auto waitForPacket = [&] { @@ -257,6 +281,42 @@ int main(int argc, char** argv) { collect(); Require(Protection(buffer) == PAGE_READONLY && Protection(buffer + capacity - 1) == PAGE_READONLY, "failed receive releases the host write guard"); + if (api == 6) { + Require(sceKernelMprotect(static_cast(mapping) + page, page, 1) == 0, + "make the later receive vector read-only"); + send(); + waitForPacket(); + Require(receive(buffer, capacity, 0, nullptr, nullptr) == static_cast(0x8041010eu) && *error == 14, + "a later read-only vector fails with EFAULT"); + Require(std::all_of(buffer - 1, buffer + capacity + 1, [](char value) { return value == '!'; }), + "a rejected vector list leaves every destination unchanged"); + collect(); + Require(Protection(buffer) == PAGE_READONLY, "a later vector failure releases earlier guards"); + std::array recovery{}; + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload), + "a rejected vector list preserves queued data"); + Require(sceKernelMprotect(static_cast(mapping) + page, page, 3) == 0, + "restore the later receive vector"); + NetIovec vectors[]{{buffer, 3}, {nullptr, 0}, {buffer + 3, capacity - 3}}; + auto* message = reinterpret_cast(static_cast(twin) + 32); + const NetMsghdr initial{nullptr, 0, vectors, 3, nullptr, 123, -1}; + { + const GuestArena::HostWrite destination(message, sizeof(*message)); + Require(destination.Open(), "initialize the receive message"); + std::memcpy(message, &initial, sizeof(initial)); + } + Require(sceKernelMprotect(twin, page, 1) == 0, "make the receive message read-only"); + send(); + waitForPacket(); + Require(sceNetRecvmsg(receiver, message, 0) == static_cast(0x8041010eu) && *error == 14, + "read-only message outputs fail with EFAULT"); + Require(message->flags == -1 && message->controlLength == 123, "rejected message outputs unchanged"); + Require(receive(recovery.data(), recovery.size(), 0, nullptr, nullptr) == sizeof(payload), + "a rejected message preserves queued data"); + Require(sceKernelMprotect(twin, page, 3) == 0, "restore receive message write permission"); + reset(); + collect(); + } Require(sceKernelMprotect(mapping, page * 2, 1) == 0, "make guest memory read-only"); send(); waitForPacket(); From df91160af8dbd978320641a8ce3c58bf58db8dc9 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:36:48 +0000 Subject: [PATCH 004/131] feat(agc): draw a performance overlay over presented frames ANYPS5_OVERLAY=1 shows frames per second over the last second, the last frame time, a graph of the last 120 frame times with their minimum, average and maximum, and with APS5_PROFILE_GPU and APS5_PROFILE_DRAW the GPU and driver time per frame, in the corner ANYPS5_OVERLAY_POS names. A 5x7 bitmap font and solid rectangles are drawn as instanced quads by one render pass after the frame reaches the swapchain image, on the presentation pass and the blit paths alike, in SDR and at 203 nits on an HDR10 swapchain. The text scales with the swapchain height. While hidden, the presentation records nothing for it. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 60 ++ .../Execution/include/PerformanceOverlay.hpp | 166 +++++ .../Execution/include/Presentation.hpp | 1 + .../src/Driver/Packets/PacketExecution.cpp | 2 + .../Execution/src/PerformanceOverlay.cpp | 584 ++++++++++++++++++ .../Execution/src/VulkanDevice.cpp | 12 +- .../Graphics/shaders/PerformanceOverlay.frag | 12 + .../Graphics/shaders/PerformanceOverlay.vert | 16 + .../libSceAgcDriver/Graphics/src/Recorder.cpp | 2 + .../tests/PerformanceOverlay.cpp | 518 ++++++++++++++++ .../tests/PerformanceOverlayStatistics.cpp | 238 +++++++ 11 files changed, 1609 insertions(+), 2 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert create mode 100644 core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index dca6910043..b798358b13 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -125,6 +125,23 @@ foreach(agcPresentationStage IN ITEMS Vert Frag) ) list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader}) endforeach() +foreach(agcOverlayStage IN ITEMS Vert Frag) + string(TOLOWER "${agcOverlayStage}" agcOverlayExtension) + string(TOUPPER "${agcOverlayStage}" agcOverlaySymbol) + set(agcOverlayShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/PerformanceOverlay.${agcOverlayExtension}) + set(agcOverlayShaderSpv ${agcTextureDetileGeneratedDir}/PerformanceOverlay${agcOverlayStage}.spv) + set(agcOverlayShaderHeader ${agcTextureDetileGeneratedDir}/PerformanceOverlay${agcOverlayStage}_spv.h) + add_custom_command( + OUTPUT "${agcOverlayShaderHeader}" + COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}" + COMMAND "$" -V --target-env vulkan1.1 -o "${agcOverlayShaderSpv}" "${agcOverlayShaderSource}" + COMMAND ${CMAKE_COMMAND} -DINPUT=${agcOverlayShaderSpv} -DOUTPUT=${agcOverlayShaderHeader} + -DSYMBOL=PERFORMANCE_OVERLAY_${agcOverlaySymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake" + DEPENDS "${agcOverlayShaderSource}" glslang-standalone + VERBATIM + ) + list(APPEND agcPresentationShaderHeaders ${agcOverlayShaderHeader}) +endforeach() set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Unimplemented.cpp @@ -194,6 +211,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationPass.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceOverlay.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp @@ -1183,6 +1201,48 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_performance_overlay_statistics_tests EXCLUDE_FROM_ALL + tests/PerformanceOverlayStatistics.cpp +) +target_include_directories(agc_performance_overlay_statistics_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_performance_overlay_statistics_tests PRIVATE + libSceAgcDriver + libc +) +set_target_properties(agc_performance_overlay_statistics_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_performance_overlay_statistics_tests) + +add_test_executable(agc_performance_overlay_tests EXCLUDE_FROM_ALL + tests/PerformanceOverlay.cpp +) +target_include_directories(agc_performance_overlay_tests PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/core/shader/recompiler + ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} +) +target_link_libraries(agc_performance_overlay_tests PRIVATE + libSceAgcDriver + SDL2-static + libc +) +set_target_properties(agc_performance_overlay_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_performance_overlay_tests) + +if(BUILD_TESTING) + set_target_properties(agc_performance_overlay_statistics_tests agc_performance_overlay_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_performance_overlay_statistics COMMAND agc_performance_overlay_statistics_tests) + add_test(NAME agc_performance_overlay COMMAND agc_performance_overlay_tests) + set_tests_properties(agc_performance_overlay PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + if(WIN32) + set_tests_properties(agc_performance_overlay_statistics agc_performance_overlay PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_driver_pm4_tests EXCLUDE_FROM_ALL tests/Pm4.cpp ) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp new file mode 100644 index 0000000000..f99d27af9f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -0,0 +1,166 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCEOVERLAY_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCEOVERLAY_HPP + +#define VK_NO_PROTOTYPES +#include +#include "prx/libSceAgcDriver/Graphics/include/Context.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { + +namespace Graphics { +class Buffer; +} + +enum class OverlayCorner : std::uint8_t { TopLeft, TopRight, BottomLeft, BottomRight }; + +struct OverlaySettings { + bool visible = false; + OverlayCorner corner = OverlayCorner::TopLeft; +}; + +OverlaySettings ParseOverlaySettings(const char* enabled, const char* position); + +class PerformanceCounters { +public: + static void AddGpuNanoseconds(std::uint64_t nanoseconds); + static void AddDriverNanoseconds(std::uint64_t nanoseconds); + static std::uint64_t GpuNanoseconds(); + static std::uint64_t DriverNanoseconds(); + static bool GpuEnabled(); + static bool DriverEnabled(); +}; + +class OverlayStatistics { +public: + using Clock = std::chrono::steady_clock; + static constexpr std::size_t FrameHistory = 120; + static constexpr Clock::duration Window = std::chrono::seconds(1); + + struct Range { + double minimum = 0; + double average = 0; + double maximum = 0; + }; + + void AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds); + void Reset(); + double Fps() const; + double LastFrameMs() const; + std::vector FrameTimes() const; + Range FrameTimeRange() const; + double GpuMsPerFrame() const; + double DriverMsPerFrame() const; + +private: + struct Sample { + Clock::time_point at; + std::uint64_t gpu = 0; + std::uint64_t driver = 0; + }; + + double perFrameMs(std::uint64_t Sample::*counter) const; + + std::deque window; + std::array frameTimes{}; + std::size_t frameCount = 0; + std::size_t frameNext = 0; +}; + +struct OverlayRect { + std::int32_t x = 0; + std::int32_t y = 0; + std::int32_t width = 0; + std::int32_t height = 0; + std::uint32_t color = 0; +}; + +enum class OverlayColor : std::uint32_t { Background = 0, Text = 1, Graph = 2 }; +inline constexpr std::size_t OverlayColorCount = 3; + +struct OverlayContent { + std::vector above; + std::vector frameTimes; + std::vector below; +}; + +struct OverlayLayout { + OverlayRect bounds; + std::vector rects; + std::int32_t scale = 1; +}; + +struct OverlayMetrics { + static constexpr std::int32_t GlyphWidth = 5; + static constexpr std::int32_t GlyphHeight = 7; + static constexpr std::int32_t Advance = 6; + static constexpr std::int32_t LinePitch = 9; + static constexpr std::int32_t Columns = 27; + static constexpr std::int32_t Padding = 3; + static constexpr std::int32_t Margin = 4; + static constexpr std::int32_t GraphHeight = 20; + static constexpr std::int32_t GraphGap = 2; + static constexpr std::int32_t InnerWidth = Columns * Advance - 1; +}; + +const std::array& OverlayGlyph(char character); +std::string FormatOverlayValue(double value); +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver); +std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); +OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); +std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace); + +class PerformanceOverlay { +public: + PerformanceOverlay(const Graphics::Context& context, std::size_t slots); + ~PerformanceOverlay(); + PerformanceOverlay(const PerformanceOverlay&) = delete; + PerformanceOverlay& operator=(const PerformanceOverlay&) = delete; + + static bool Visible(); + static void Toggle(); + static OverlayCorner Corner(); + + void NotePresent(OverlayStatistics::Clock::time_point at); + void NotePresent(OverlayStatistics::Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds); + const OverlayStatistics& Statistics() const { return statistics; } + OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout); + OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner); + void Release(std::size_t slot) noexcept; + +private: + struct Slot { + std::unique_ptr rects; + std::size_t capacity = 0; + std::vector> transient; + }; + + VkRenderPass renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout); + VkPipeline pipeline(VkFormat format, VkRenderPass compatible); + void releaseTransient(Slot& slot) noexcept; + void release() noexcept; + + Graphics::Context context; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkShaderModule vertex = VK_NULL_HANDLE; + VkShaderModule fragment = VK_NULL_HANDLE; + std::map, VkRenderPass> renderPasses; + std::map pipelines; + std::vector slots; + OverlayStatistics statistics; + std::uint64_t toggles = 0; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp index 3f771fbb1a..3cdc977947 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Presentation.hpp @@ -29,6 +29,7 @@ extern "C" void AgcDriverAttachWindow_nid_postfix(const AgcDriver::PresentationW extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, void (*gpuReady)(void*), void* context); extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window); +extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix(); extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error); #endif 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 492a2e4095..66f9d6d4b6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Eq/include/Event.hpp" @@ -23,6 +24,7 @@ void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { const auto end = std::chrono::steady_clock::now(); profile.*bucket += std::chrono::duration(end - begin).count(); static const bool report = std::getenv("APS5_PROFILE_DRAW") != nullptr; + if (report && bucket != &WorkerProfile::waitMs) PerformanceCounters::AddDriverNanoseconds(static_cast(std::chrono::duration_cast(end - begin).count())); if (report && end - profile.reported > std::chrono::seconds(10)) { profile.reported = end; AgcDriver::ProfilePrint_nid_no_patch( "[gpu] worker at %.0f s: dispatch %.1f s, draw %.1f s, wait %.1f s\n", std::chrono::duration(end - profile.start).count(), profile.dispatchMs / 1000, profile.drawMs / 1000, profile.waitMs / 1000); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp new file mode 100644 index 0000000000..f9218396a3 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -0,0 +1,584 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" +#include "prx/libSceAgcDriver/Execution/include/PqToSdr.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayFrag_spv.h" +#include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayVert_spv.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver { +namespace { + +static_assert(sizeof(OverlayRect) == 20, "overlay rectangles are read as one ivec4 and one uint per instance"); + +struct OverlayParameters { + std::array, OverlayColorCount> palette; + std::array extent; +}; + +std::atomic toggleCount{0}; +std::atomic gpuTotal{0}; +std::atomic driverTotal{0}; + +const OverlaySettings& settings() { + static const OverlaySettings parsed = ParseOverlaySettings(std::getenv("ANYPS5_OVERLAY"), std::getenv("ANYPS5_OVERLAY_POS")); + return parsed; +} + +std::int32_t panelWidth(std::int32_t scale) { + return (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * scale; +} + +std::int32_t panelHeight(const OverlayContent& content, std::int32_t scale) { + const auto lines = static_cast(content.above.size() + content.below.size()); + const auto glyphGap = OverlayMetrics::LinePitch - OverlayMetrics::GlyphHeight; + return (2 * OverlayMetrics::Padding + lines * OverlayMetrics::LinePitch + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap - glyphGap) * scale; +} + +bool fits(VkExtent2D extent, const OverlayContent& content, std::int32_t scale) { + const auto margins = 2 * OverlayMetrics::Margin * scale; + return panelWidth(scale) + margins <= static_cast(extent.width) && panelHeight(content, scale) + margins <= static_cast(extent.height); +} + +float srgbDecode(float value) { + return value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f); +} + +float pqEncode(float nits) { + constexpr double m1 = 2610.0 / 16384.0; + constexpr double m2 = 2523.0 / 4096.0 * 128.0; + constexpr double c1 = 3424.0 / 4096.0; + constexpr double c2 = 2413.0 / 4096.0 * 32.0; + constexpr double c3 = 2392.0 / 4096.0 * 32.0; + const double power = std::pow(std::clamp(static_cast(nits) / 10000.0, 0.0, 1.0), m1); + return static_cast(std::pow((c1 + c2 * power) / (1.0 + c3 * power), m2)); +} + +bool srgbFormat(VkFormat format) { + return format == VK_FORMAT_B8G8R8A8_SRGB || format == VK_FORMAT_R8G8B8A8_SRGB || format == VK_FORMAT_A8B8G8R8_SRGB_PACK32; +} + +} + +OverlaySettings ParseOverlaySettings(const char* enabled, const char* position) { + OverlaySettings parsed; + const std::string_view visible = enabled != nullptr ? enabled : ""; + if (visible == "1") parsed.visible = true; + else if (!visible.empty() && visible != "0") throw std::runtime_error("ANYPS5_OVERLAY must be 1 or 0, not '" + std::string(visible) + "'"); + const std::string_view corner = position != nullptr ? position : ""; + if (corner.empty() || corner == "tl") parsed.corner = OverlayCorner::TopLeft; + else if (corner == "tr") parsed.corner = OverlayCorner::TopRight; + else if (corner == "bl") parsed.corner = OverlayCorner::BottomLeft; + else if (corner == "br") parsed.corner = OverlayCorner::BottomRight; + else throw std::runtime_error("ANYPS5_OVERLAY_POS must be tl, tr, bl or br, not '" + std::string(corner) + "'"); + return parsed; +} + +void PerformanceCounters::AddGpuNanoseconds(std::uint64_t nanoseconds) { + gpuTotal.fetch_add(nanoseconds, std::memory_order_relaxed); +} + +void PerformanceCounters::AddDriverNanoseconds(std::uint64_t nanoseconds) { + driverTotal.fetch_add(nanoseconds, std::memory_order_relaxed); +} + +std::uint64_t PerformanceCounters::GpuNanoseconds() { + return gpuTotal.load(std::memory_order_relaxed); +} + +std::uint64_t PerformanceCounters::DriverNanoseconds() { + return driverTotal.load(std::memory_order_relaxed); +} + +bool PerformanceCounters::GpuEnabled() { + return Graphics::Recorder::GpuTimingEnabled(); +} + +bool PerformanceCounters::DriverEnabled() { + static const bool enabled = std::getenv("APS5_PROFILE_DRAW") != nullptr; + return enabled; +} + +void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) { + if (!window.empty()) { + Graphics::Require(at >= window.back().at, "presents reached the performance overlay out of order"); + frameTimes[frameNext] = std::chrono::duration(at - window.back().at).count(); + frameNext = (frameNext + 1) % FrameHistory; + frameCount = std::min(frameCount + 1, FrameHistory); + } + window.push_back({at, gpuNanoseconds, driverNanoseconds}); + while (at - window.front().at > Window) window.pop_front(); +} + +void OverlayStatistics::Reset() { + window.clear(); + frameTimes = {}; + frameCount = 0; + frameNext = 0; +} + +double OverlayStatistics::Fps() const { + if (window.size() < 2) return 0; + const auto seconds = std::chrono::duration(window.back().at - window.front().at).count(); + return seconds > 0 ? static_cast(window.size() - 1) / seconds : 0; +} + +double OverlayStatistics::LastFrameMs() const { + return frameCount == 0 ? 0 : frameTimes[(frameNext + FrameHistory - 1) % FrameHistory]; +} + +std::vector OverlayStatistics::FrameTimes() const { + std::vector ordered; + ordered.reserve(frameCount); + for (std::size_t i = 0; i < frameCount; ++i) ordered.push_back(frameTimes[(frameNext + FrameHistory - frameCount + i) % FrameHistory]); + return ordered; +} + +OverlayStatistics::Range OverlayStatistics::FrameTimeRange() const { + const auto times = FrameTimes(); + if (times.empty()) return {}; + Range range{times.front(), 0, times.front()}; + double sum = 0; + for (const auto time : times) { + range.minimum = std::min(range.minimum, time); + range.maximum = std::max(range.maximum, time); + sum += time; + } + range.average = sum / static_cast(times.size()); + return range; +} + +double OverlayStatistics::perFrameMs(std::uint64_t Sample::*counter) const { + if (window.size() < 2) return 0; + const auto first = window.front().*counter; + const auto last = window.back().*counter; + return last >= first ? static_cast(last - first) / 1e6 / static_cast(window.size() - 1) : 0; +} + +double OverlayStatistics::GpuMsPerFrame() const { + return perFrameMs(&Sample::gpu); +} + +double OverlayStatistics::DriverMsPerFrame() const { + return perFrameMs(&Sample::driver); +} + +const std::array& OverlayGlyph(char character) { + using Glyph = std::array; + static constexpr Glyph space{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + static constexpr std::array digits{{ + {0x0e, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0e}, + {0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e}, + {0x0e, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1f}, + {0x1f, 0x02, 0x04, 0x02, 0x01, 0x11, 0x0e}, + {0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02}, + {0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e}, + {0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e}, + {0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08}, + {0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e}, + {0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c}, + }}; + static constexpr Glyph period{0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c}; + static constexpr Glyph colon{0x00, 0x0c, 0x0c, 0x00, 0x0c, 0x0c, 0x00}; + static constexpr Glyph slash{0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00}; + static constexpr Glyph a{0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11}; + static constexpr Glyph d{0x1c, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1c}; + static constexpr Glyph e{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f}; + static constexpr Glyph f{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10}; + static constexpr Glyph g{0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f}; + static constexpr Glyph i{0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e}; + static constexpr Glyph m{0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11}; + static constexpr Glyph n{0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11}; + static constexpr Glyph p{0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10}; + static constexpr Glyph r{0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11}; + static constexpr Glyph s{0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e}; + static constexpr Glyph u{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; + static constexpr Glyph v{0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04}; + static constexpr Glyph x{0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11}; + if (character >= '0' && character <= '9') return digits[static_cast(character - '0')]; + switch (character) { + case ' ': return space; + case '.': return period; + case ':': return colon; + case '/': return slash; + case 'A': return a; + case 'D': return d; + case 'E': return e; + case 'F': return f; + case 'G': return g; + case 'I': return i; + case 'M': return m; + case 'N': return n; + case 'P': return p; + case 'R': return r; + case 'S': return s; + case 'U': return u; + case 'V': return v; + case 'X': return x; + default: throw std::runtime_error(std::string("the performance overlay font has no glyph for '") + character + "'"); + } +} + +std::string FormatOverlayValue(double value) { + const double clamped = std::isfinite(value) ? std::clamp(value, 0.0, 9999.0) : 0.0; + char text[16]; + std::snprintf(text, sizeof(text), clamped < 99.95 ? "%.1f" : "%.0f", clamped); + return text; +} + +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver) { + OverlayContent content; + content.above.push_back("FPS: " + FormatOverlayValue(statistics.Fps())); + content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS"); + content.frameTimes = statistics.FrameTimes(); + const auto range = statistics.FrameTimeRange(); + content.below.push_back("MIN/AVG/MAX: " + FormatOverlayValue(range.minimum) + "/" + FormatOverlayValue(range.average) + "/" + FormatOverlayValue(range.maximum)); + if (gpu) content.below.push_back("GPU: " + FormatOverlayValue(statistics.GpuMsPerFrame()) + " MS"); + if (driver) content.below.push_back("DRIVER: " + FormatOverlayValue(statistics.DriverMsPerFrame()) + " MS"); + return content; +} + +std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { + auto scale = std::max(1, static_cast((extent.height + 240) / 480)); + while (scale > 1 && !fits(extent, content, scale)) --scale; + return scale; +} + +OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner) { + Graphics::Require(extent.width != 0 && extent.height != 0, "the performance overlay needs a destination"); + Graphics::Require(content.frameTimes.size() <= OverlayStatistics::FrameHistory, "the performance overlay graph holds at most 120 frames"); + OverlayLayout layout; + const auto scale = OverlayScale(extent, content); + layout.scale = scale; + const auto width = panelWidth(scale); + const auto height = panelHeight(content, scale); + const auto margin = OverlayMetrics::Margin * scale; + const bool right = corner == OverlayCorner::TopRight || corner == OverlayCorner::BottomRight; + const bool bottom = corner == OverlayCorner::BottomLeft || corner == OverlayCorner::BottomRight; + const auto left = std::max(0, right ? static_cast(extent.width) - margin - width : margin); + const auto top = std::max(0, bottom ? static_cast(extent.height) - margin - height : margin); + layout.bounds = {left, top, width, height, static_cast(OverlayColor::Background)}; + layout.rects.push_back(layout.bounds); + const auto text = [&](const std::string& line, std::int32_t row) { + Graphics::Require(line.size() <= static_cast(OverlayMetrics::Columns), "a performance overlay line is wider than the panel"); + for (std::size_t column = 0; column < line.size(); ++column) { + const auto& glyph = OverlayGlyph(line[column]); + const auto glyphLeft = left + (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance) * scale; + for (std::int32_t y = 0; y < OverlayMetrics::GlyphHeight; ++y) { + const auto bits = glyph[static_cast(y)]; + std::int32_t x = 0; + while (x < OverlayMetrics::GlyphWidth) { + if ((bits & (0x10u >> x)) == 0) { + ++x; + continue; + } + auto end = x; + while (end < OverlayMetrics::GlyphWidth && (bits & (0x10u >> end)) != 0) ++end; + layout.rects.push_back({glyphLeft + x * scale, top + (row + y) * scale, (end - x) * scale, scale, static_cast(OverlayColor::Text)}); + x = end; + } + } + } + }; + auto row = OverlayMetrics::Padding; + for (const auto& line : content.above) { + text(line, row); + row += OverlayMetrics::LinePitch; + } + const auto graphBottom = top + (row + OverlayMetrics::GraphHeight) * scale; + if (!content.frameTimes.empty()) { + double maximum = 0; + double sum = 0; + for (const auto time : content.frameTimes) { + maximum = std::max(maximum, time); + sum += time; + } + const double fullScale = std::max(maximum, 2.0 * sum / static_cast(content.frameTimes.size())); + const auto first = static_cast(OverlayStatistics::FrameHistory - content.frameTimes.size()); + for (std::size_t i = 0; i < content.frameTimes.size() && fullScale > 0; ++i) { + const auto pixels = std::clamp(static_cast(std::lround(content.frameTimes[i] / fullScale * OverlayMetrics::GraphHeight * scale)), 1, OverlayMetrics::GraphHeight * scale); + layout.rects.push_back({left + (OverlayMetrics::Padding + first + static_cast(i)) * scale, graphBottom - pixels, scale, pixels, static_cast(OverlayColor::Graph)}); + } + } + row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + text(line, row); + row += OverlayMetrics::LinePitch; + } + return layout; +} + +std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace) { + std::array, OverlayColorCount> palette{{{0.0f, 0.0f, 0.0f, 0.6f}, {1.0f, 1.0f, 1.0f, 1.0f}, {0.35f, 0.8f, 0.35f, 1.0f}}}; + if (colorSpace == VK_COLOR_SPACE_HDR10_ST2084_EXT) { + for (auto& color : palette) { + const float red = srgbDecode(color[0]); + const float green = srgbDecode(color[1]); + const float blue = srgbDecode(color[2]); + const std::array bt2020{ + 0.6274f * red + 0.3293f * green + 0.0433f * blue, + 0.0691f * red + 0.9195f * green + 0.0114f * blue, + 0.0164f * red + 0.0880f * green + 0.8956f * blue}; + for (std::size_t channel = 0; channel < 3; ++channel) color[channel] = pqEncode(bt2020[channel] * PqReferenceWhiteNits); + } + } else if (srgbFormat(format)) { + for (auto& color : palette) { + for (std::size_t channel = 0; channel < 3; ++channel) color[channel] = srgbDecode(color[channel]); + } + } + return palette; +} + +PerformanceOverlay::PerformanceOverlay(const Graphics::Context& context, std::size_t slotCount) : context(context), slots(slotCount) { + Graphics::Require(slotCount != 0, "the performance overlay needs at least one slot"); + try { + const VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(OverlayParameters)}; + VkPipelineLayoutCreateInfo layoutInfo{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; + layoutInfo.pushConstantRangeCount = 1; + layoutInfo.pPushConstantRanges = &push; + Graphics::Check(context.Function("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout performance overlay"); + const auto createModule = context.Function("vkCreateShaderModule"); + VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO}; + moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_VERT_SPV); + moduleInfo.pCode = PERFORMANCE_OVERLAY_VERT_SPV; + Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule performance overlay vertex"); + moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_FRAG_SPV); + moduleInfo.pCode = PERFORMANCE_OVERLAY_FRAG_SPV; + Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule performance overlay fragment"); + } catch (...) { + release(); + throw; + } +} + +PerformanceOverlay::~PerformanceOverlay() { + release(); +} + +void PerformanceOverlay::releaseTransient(Slot& slot) noexcept { + for (const auto& [view, framebuffer] : slot.transient) { + if (framebuffer) context.Function("vkDestroyFramebuffer")(context.device, framebuffer, nullptr); + if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); + } + slot.transient.clear(); +} + +void PerformanceOverlay::Release(std::size_t index) noexcept { + if (index < slots.size()) releaseTransient(slots[index]); +} + +void PerformanceOverlay::release() noexcept { + for (auto& slot : slots) { + releaseTransient(slot); + slot.rects.reset(); + slot.capacity = 0; + } + for (const auto& [format, entry] : pipelines) context.Function("vkDestroyPipeline")(context.device, entry, nullptr); + pipelines.clear(); + for (const auto& [key, entry] : renderPasses) context.Function("vkDestroyRenderPass")(context.device, entry, nullptr); + renderPasses.clear(); + if (fragment) context.Function("vkDestroyShaderModule")(context.device, fragment, nullptr); + if (vertex) context.Function("vkDestroyShaderModule")(context.device, vertex, nullptr); + if (pipelineLayout) context.Function("vkDestroyPipelineLayout")(context.device, pipelineLayout, nullptr); + fragment = vertex = VK_NULL_HANDLE; + pipelineLayout = VK_NULL_HANDLE; +} + +bool PerformanceOverlay::Visible() { + return settings().visible != ((toggleCount.load(std::memory_order_relaxed) & 1u) != 0); +} + +void PerformanceOverlay::Toggle() { + toggleCount.fetch_add(1, std::memory_order_relaxed); +} + +OverlayCorner PerformanceOverlay::Corner() { + return settings().corner; +} + +void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at) { + NotePresent(at, PerformanceCounters::GpuEnabled() ? PerformanceCounters::GpuNanoseconds() : 0, PerformanceCounters::DriverEnabled() ? PerformanceCounters::DriverNanoseconds() : 0); +} + +void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) { + const auto current = toggleCount.load(std::memory_order_relaxed); + if (current != toggles) { + statistics.Reset(); + toggles = current; + } + statistics.AddPresent(at, gpuNanoseconds, driverNanoseconds); +} + +VkRenderPass PerformanceOverlay::renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout) { + const auto key = std::tuple{format, initialLayout, finalLayout}; + if (const auto found = renderPasses.find(key); found != renderPasses.end()) return found->second; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + Graphics::Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) != 0, "the performance overlay cannot blend into this format"); + VkAttachmentDescription attachment{}; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; + 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 = initialLayout; + attachment.finalLayout = finalLayout; + const VkAttachmentReference reference{0, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL}; + VkSubpassDescription subpass{}; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &reference; + const bool transfer = finalLayout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + const VkSubpassDependency dependencies[2] = { + {VK_SUBPASS_EXTERNAL, 0, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, 0}, + {0, VK_SUBPASS_EXTERNAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, transfer ? VK_PIPELINE_STAGE_TRANSFER_BIT : VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, transfer ? VK_ACCESS_TRANSFER_READ_BIT : VkAccessFlags{0}, 0}, + }; + VkRenderPassCreateInfo info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO}; + info.attachmentCount = 1; + info.pAttachments = &attachment; + info.subpassCount = 1; + info.pSubpasses = &subpass; + info.dependencyCount = 2; + info.pDependencies = dependencies; + VkRenderPass created = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateRenderPass")(context.device, &info, nullptr, &created), "vkCreateRenderPass performance overlay"); + return renderPasses.emplace(key, created).first->second; +} + +VkPipeline PerformanceOverlay::pipeline(VkFormat format, VkRenderPass compatible) { + if (const auto found = pipelines.find(format); found != pipelines.end()) return found->second; + 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"; + const VkVertexInputBindingDescription binding{0, sizeof(OverlayRect), VK_VERTEX_INPUT_RATE_INSTANCE}; + const VkVertexInputAttributeDescription attributes[2] = {{0, 0, VK_FORMAT_R32G32B32A32_SINT, 0}, {1, 0, VK_FORMAT_R32_UINT, 16}}; + VkPipelineVertexInputStateCreateInfo vertexInput{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO}; + vertexInput.vertexBindingDescriptionCount = 1; + vertexInput.pVertexBindingDescriptions = &binding; + vertexInput.vertexAttributeDescriptionCount = 2; + vertexInput.pVertexAttributeDescriptions = attributes; + VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + VkPipelineViewportStateCreateInfo viewport{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + viewport.viewportCount = 1; + viewport.scissorCount = 1; + 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.blendEnable = VK_TRUE; + blendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; + blendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + blendAttachment.colorBlendOp = VK_BLEND_OP_ADD; + blendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; + blendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE; + blendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; + 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; + const VkDynamicState dynamicStates[2] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}; + VkPipelineDynamicStateCreateInfo dynamic{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO}; + dynamic.dynamicStateCount = 2; + dynamic.pDynamicStates = dynamicStates; + VkGraphicsPipelineCreateInfo info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + info.stageCount = 2; + info.pStages = stages; + info.pVertexInputState = &vertexInput; + info.pInputAssemblyState = &assembly; + info.pViewportState = &viewport; + info.pRasterizationState = &rasterization; + info.pMultisampleState = &multisample; + info.pColorBlendState = &blend; + info.pDynamicState = &dynamic; + info.layout = pipelineLayout; + info.renderPass = compatible; + VkPipeline created = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateGraphicsPipelines")(context.device, VK_NULL_HANDLE, 1, &info, nullptr, &created), "vkCreateGraphicsPipelines performance overlay"); + return pipelines.emplace(format, created).first->second; +} + +OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout) { + return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlay(statistics, PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled()), Corner()); +} + +OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner) { + Graphics::Require(index < slots.size(), "performance overlay slot is out of range"); + Graphics::Require(destination != VK_NULL_HANDLE && extent.width != 0 && extent.height != 0, "the performance overlay destination is unavailable"); + auto& slot = slots[index]; + releaseTransient(slot); + const auto layout = LayOutOverlay(content, extent, corner); + if (slot.capacity < layout.rects.size()) { + const auto capacity = std::max(layout.rects.size(), 1024); + slot.rects = std::make_unique(context, capacity * sizeof(OverlayRect), VK_BUFFER_USAGE_VERTEX_BUFFER_BIT); + slot.capacity = capacity; + } + std::memcpy(slot.rects->Bytes().data(), layout.rects.data(), layout.rects.size() * sizeof(OverlayRect)); + const auto pass = renderPass(format, initialLayout, finalLayout); + const auto drawing = pipeline(format, pass); + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = destination; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.components = {VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY}; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; + usageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + viewInfo.pNext = &usageInfo; + VkImageView view = VK_NULL_HANDLE; + Graphics::Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView performance overlay"); + VkFramebufferCreateInfo framebufferInfo{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO}; + framebufferInfo.renderPass = pass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = &view; + framebufferInfo.width = extent.width; + framebufferInfo.height = extent.height; + framebufferInfo.layers = 1; + VkFramebuffer framebuffer = VK_NULL_HANDLE; + const auto created = context.Function("vkCreateFramebuffer")(context.device, &framebufferInfo, nullptr, &framebuffer); + slot.transient.emplace_back(view, framebuffer); + Graphics::Check(created, "vkCreateFramebuffer performance overlay"); + const auto right = std::min(layout.bounds.x + layout.bounds.width, static_cast(extent.width)); + const auto bottom = std::min(layout.bounds.y + layout.bounds.height, static_cast(extent.height)); + const VkRect2D area{{layout.bounds.x, layout.bounds.y}, {static_cast(std::max(1, right - layout.bounds.x)), static_cast(std::max(1, bottom - layout.bounds.y))}}; + VkRenderPassBeginInfo begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO}; + begin.renderPass = pass; + begin.framebuffer = framebuffer; + begin.renderArea = area; + context.Function("vkCmdBeginRenderPass")(commands, &begin, VK_SUBPASS_CONTENTS_INLINE); + context.Function("vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_GRAPHICS, drawing); + const VkViewport viewport{0.0f, 0.0f, static_cast(extent.width), static_cast(extent.height), 0.0f, 1.0f}; + context.Function("vkCmdSetViewport")(commands, 0, 1, &viewport); + context.Function("vkCmdSetScissor")(commands, 0, 1, &area); + const OverlayParameters parameters{OverlayPalette(format, colorSpace), {static_cast(extent.width), static_cast(extent.height)}}; + context.Function("vkCmdPushConstants")(commands, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(parameters), ¶meters); + const VkBuffer buffer = slot.rects->Handle(); + const VkDeviceSize offset = 0; + context.Function("vkCmdBindVertexBuffers")(commands, 0, 1, &buffer, &offset); + context.Function("vkCmdDraw")(commands, 4, static_cast(layout.rects.size()), 0, 0); + context.Function("vkCmdEndRenderPass")(commands); + return layout.bounds; +} + +} + +extern "C" void AgcDriverTogglePerformanceOverlay_nid_postfix() { + AgcDriver::PerformanceOverlay::Toggle(); +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index c8830d353d..bc614c2e0a 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -15,6 +15,7 @@ #include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp" #include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -372,6 +373,7 @@ struct VulkanDevice::State { VkPhysicalDeviceMeshShaderPropertiesEXT meshLimits{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_PROPERTIES_EXT}; std::unique_ptr scaler; std::unique_ptr presentation; + std::unique_ptr overlay; VkFormat surfaceFormat = VK_FORMAT_B8G8R8A8_UNORM; VkColorSpaceKHR surfaceColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; bool swapchainColorSpaces = false; @@ -723,6 +725,7 @@ struct VulkanDevice::State { textureCache.reset(); detiler.reset(); colorTransfer.reset(); + overlay.reset(); presentation.reset(); scaler.reset(); hdrScaler.reset(); @@ -2944,6 +2947,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu if (state->presentSlots[state->presentCursor].inFlight) RetirePresents(state->presentSlots.size() - 1); auto& slot = state->presentSlots[state->presentCursor]; require(!slot.inFlight, "presentation slot is still in flight"); + const bool overlay = PerformanceOverlay::Visible(); if (display != nullptr && resident == nullptr) { static_cast(DisplayBufferSize(*display)); state->colorTransfer->Upload(display->address, width, height, Graphics::ColorTileMode::RenderTarget, DisplayTexelBytes(display->pixelFormat)); @@ -3035,7 +3039,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu } if (drawn) { state->presentation->Bind(slotIndex, resident->Image(), width, height, display->pixelFormat, whiteNits, residentLayer); - state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + state->presentation->RecordPresent(commands, slotIndex, barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, overlay ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); } else { scaler->RecordLetterbox(commands, width, height, barrier.image, state->extent.width, state->extent.height); if (direct) PresentationScaler::RecordBlitFrom(graphicsContext(), commands, resident->Image(), VK_IMAGE_LAYOUT_GENERAL, width, height, residentFilter, barrier.image, state->extent.width, state->extent.height, residentLayer); @@ -3073,7 +3077,11 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); } } - if (!drawn) { + if (overlay) { + if (!state->overlay) state->overlay = std::make_unique(graphicsContext(), state->presentSlots.size()); + state->overlay->NotePresent(std::chrono::steady_clock::now()); + state->overlay->Record(commands, static_cast(&slot - state->presentSlots.data()), barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + } else if (!drawn) { barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; barrier.dstAccessMask = 0; barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag new file mode 100644 index 0000000000..33f1f9740d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag @@ -0,0 +1,12 @@ +#version 450 + +layout(push_constant) uniform Parameters { + vec4 palette[3]; + vec2 extent; +} parameters; +layout(location = 0) flat in uint paletteIndex; +layout(location = 0) out vec4 color; + +void main() { + color = parameters.palette[paletteIndex]; +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert new file mode 100644 index 0000000000..b59069433c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.vert @@ -0,0 +1,16 @@ +#version 450 + +layout(location = 0) in ivec4 area; +layout(location = 1) in uint color; +layout(push_constant) uniform Parameters { + vec4 palette[3]; + vec2 extent; +} parameters; +layout(location = 0) flat out uint paletteIndex; + +void main() { + vec2 corner = vec2(float(gl_VertexIndex & 1), float((gl_VertexIndex >> 1) & 1)); + vec2 pixel = vec2(area.xy) + corner * vec2(area.zw); + gl_Position = vec4(pixel / parameters.extent * 2.0 - 1.0, 0.0, 1.0); + paletteIndex = color; +} diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index e329a0d449..6c4f039b89 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -9,6 +9,7 @@ #include "prx/libSceAgcDriver/Graphics/shaders/SampleCounter_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/SampleDumps_spv.h" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp" #include "prx/libc/include/CpuTopology.hpp" @@ -2390,6 +2391,7 @@ void Recorder::readGpuTiming(Batch& batch) { batch.gpuEndNs = static_cast(stamps[i * 2 + 1]) * period; PerformanceTrace::GpuRange("batch", batch.gpuStartNs, batch.gpuEndNs, batch.traceId); timingBatchMs += ns / 1e6; + AgcDriver::PerformanceCounters::AddGpuNanoseconds(static_cast(ns)); continue; } const auto key = batch.timedKeys[i]; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp new file mode 100644 index 0000000000..b3e573052b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -0,0 +1,518 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp" +#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" +#include "prx/libSceAgcDriver/Execution/include/ToneMapLuminance.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace AgcDriver::Graphics; +using AgcDriver::OverlayContent; +using AgcDriver::OverlayCorner; +using AgcDriver::OverlayMetrics; +using AgcDriver::OverlayRect; +using AgcDriver::PerformanceOverlay; +using AgcDriver::PresentationPass; + +constexpr int VulkanTestSkipped = 77; +constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; + +class Device { +public: + Device() { +#ifdef _WIN32 + library = SDL_LoadObject("vulkan-1.dll"); +#else + library = SDL_LoadObject("libvulkan.so.1"); +#endif + Require(library != nullptr, "cannot load Vulkan"); + try { + instanceProc = reinterpret_cast(SDL_LoadFunction(library, "vkGetInstanceProcAddr")); + Require(instanceProc != nullptr, "missing Vulkan instance resolver"); + VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + application.apiVersion = VK_API_VERSION_1_1; + VkInstanceCreateInfo info{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + info.pApplicationInfo = &application; + Check(function("vkCreateInstance")(&info, nullptr, &instance), "vkCreateInstance"); + std::uint32_t count = 0; + const auto enumerate = function("vkEnumeratePhysicalDevices"); + Check(enumerate(instance, &count, nullptr), "vkEnumeratePhysicalDevices"); + Require(count != 0, "no Vulkan device"); + std::vector devices(count); + Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); + context.physical = devices.front(); + const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); + queues(context.physical, &count, nullptr); + std::vector families(count); + queues(context.physical, &count, families.data()); + std::uint32_t family = 0; + while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family; + Require(family < count, "no Vulkan graphics queue"); + timestampBits = families[family].timestampValidBits; + const float priority = 1; + VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + queue.queueFamilyIndex = family; + queue.queueCount = 1; + queue.pQueuePriorities = &priority; + VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + device.queueCreateInfoCount = 1; + device.pQueueCreateInfos = &queue; + Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + context.deviceProc = function("vkGetDeviceProcAddr"); + function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + context.limits = properties.limits; + name = properties.deviceName; + context.formatProperties = function("vkGetPhysicalDeviceFormatProperties"); + context.imageFormatProperties = function("vkGetPhysicalDeviceImageFormatProperties"); + context.Function("vkGetDeviceQueue")(context.device, family, 0, &context.queue); + VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pool.queueFamilyIndex = family; + Check(context.Function("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool"); + } catch (...) { + release(); + throw; + } + } + + ~Device() { release(); } + const Context& GetContext() const { return context; } + const std::string& Name() const { return name; } + std::uint32_t TimestampBits() const { return timestampBits; } + +private: + template + TFunction function(const char* functionName) const { + const auto result = reinterpret_cast(instanceProc(instance, functionName)); + Require(result != nullptr, functionName); + return result; + } + + void release() noexcept { + if (context.pool != VK_NULL_HANDLE) context.Function("vkDestroyCommandPool")(context.device, context.pool, nullptr); + context.bufferPool.reset(); + if (context.device != VK_NULL_HANDLE) function("vkDestroyDevice")(context.device, nullptr); + if (instance != VK_NULL_HANDLE) function("vkDestroyInstance")(instance, nullptr); + if (library != nullptr) SDL_UnloadObject(library); + } + + void* library = nullptr; + PFN_vkGetInstanceProcAddr instanceProc = nullptr; + VkInstance instance = VK_NULL_HANDLE; + Context context{}; + std::string name; + std::uint32_t timestampBits = 0; +}; + +class Image { +public: + Image(const Context& context, VkFormat format, VkExtent2D extent, VkImageUsageFlags usage, VkImageCreateFlags flags = 0) : context(context), extent(extent) { + VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + info.flags = flags; + info.imageType = VK_IMAGE_TYPE_2D; + info.format = format; + info.extent = {extent.width, extent.height, 1}; + info.mipLevels = 1; + info.arrayLayers = 1; + info.samples = VK_SAMPLE_COUNT_1_BIT; + info.tiling = VK_IMAGE_TILING_OPTIMAL; + info.usage = usage; + info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage performance overlay test"); + 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); + const auto allocated = context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &memory); + if (allocated != VK_SUCCESS) context.Function("vkDestroyImage")(context.device, image, nullptr); + Check(allocated, "vkAllocateMemory performance overlay test"); + Check(context.Function("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory performance overlay test"); + } + ~Image() { + context.Function("vkDestroyImage")(context.device, image, nullptr); + context.Function("vkFreeMemory")(context.device, memory, nullptr); + } + Image(const Image&) = delete; + Image& operator=(const Image&) = delete; + VkImage Handle() const { return image; } + VkExtent2D Extent() const { return extent; } + +private: + const Context& context; + VkExtent2D extent; + VkImage image = VK_NULL_HANDLE; + VkDeviceMemory memory = VK_NULL_HANDLE; +}; + +void transition(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout from, VkImageLayout to, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) { + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.srcAccessMask = sourceAccess; + barrier.dstAccessMask = destinationAccess; + barrier.oldLayout = from; + barrier.newLayout = to; + barrier.srcQueueFamilyIndex = barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = image; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdPipelineBarrier")(commands, sourceStage, destinationStage, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + +void readImage(const Context& context, VkCommandBuffer commands, const Image& image, Buffer& destination) { + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {image.Extent().width, image.Extent().height, 1}; + context.Function("vkCmdCopyImageToBuffer")(commands, image.Handle(), VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.Handle(), 1, ©); + RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); +} + +std::vector wordsOf(Buffer& buffer, VkExtent2D extent) { + std::vector words(static_cast(extent.width) * extent.height); + std::memcpy(words.data(), buffer.Bytes().data(), words.size() * sizeof(std::uint32_t)); + return words; +} + +void setEnvironment(const char* name, const char* value) { +#ifdef _WIN32 + _putenv_s(name, value); +#else + setenv(name, value, 1); +#endif +} + +bool inside(const OverlayRect& rect, std::int32_t x, std::int32_t y) { + return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height; +} + +OverlayContent sixtyFps() { + AgcDriver::OverlayStatistics statistics; + const auto start = AgcDriver::OverlayStatistics::Clock::time_point{} + std::chrono::hours(1); + for (int i = 0; i <= 60; ++i) statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, 0, 0); + auto content = AgcDriver::ComposeOverlay(statistics, false, false); + Require(content.above.front() == "FPS: 60.0", "60 presents a second compose as '" + content.above.front() + "'"); + return content; +} + +std::vector clearAndOverlay(const Context& context, PerformanceOverlay* overlay, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, const VkClearColorValue& clear, const OverlayContent& content, OverlayCorner corner, OverlayRect* bounds) { + Image destination(context, format, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(extent.width) * extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &clear, 1, &range); + if (overlay != nullptr) { + *bounds = overlay->Record(commands, 0, destination.Handle(), format, colorSpace, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, content, corner); + } else { + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + } + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + if (overlay != nullptr) overlay->Release(0); + return wordsOf(readback, extent); +} + +void onlyInside(const std::vector& reference, const std::vector& drawn, VkExtent2D extent, const OverlayRect& bounds, const std::string& what, bool everyPixel = true) { + std::size_t changed = 0; + for (std::int32_t y = 0; y < static_cast(extent.height); ++y) { + for (std::int32_t x = 0; x < static_cast(extent.width); ++x) { + const auto index = static_cast(y) * extent.width + static_cast(x); + if (!inside(bounds, x, y)) { + Require(drawn[index] == reference[index], what + ": the overlay changed pixel " + std::to_string(x) + "," + std::to_string(y) + " outside its panel"); + continue; + } + if (drawn[index] != reference[index]) ++changed; + else Require(!everyPixel, what + ": the overlay left pixel " + std::to_string(x) + "," + std::to_string(y) + " of its panel unchanged"); + } + } + Require(changed != 0, what + ": the overlay changed nothing"); +} + +int channel(std::uint32_t word, unsigned shift) { + return static_cast((word >> shift) & 0xffu); +} + +void sdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { + const VkExtent2D extent{1280, 720}; + VkClearColorValue clear{}; + clear.float32[0] = 0.75f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.25f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, &bounds); + Require(bounds.x == AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft).bounds.x && bounds.width > 0 && bounds.height > 0, "the overlay drew another panel than its layout"); + onlyInside(reference, drawn, extent, bounds, "SDR 1280x720"); + const auto scale = AgcDriver::OverlayScale(extent, content); + const auto clearWord = reference.front(); + const std::string digits = "60.0"; + const auto firstColumn = content.above.front().find(digits); + Require(firstColumn != std::string::npos, "the FPS line has no '60.0'"); + for (std::size_t i = 0; i < digits.size(); ++i) { + const auto& glyph = AgcDriver::OverlayGlyph(digits[i]); + const auto column = static_cast(firstColumn + i); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < scale; ++py) { + for (std::int32_t px = 0; px < scale; ++px) { + const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + px; + const auto y = bounds.y + (OverlayMetrics::Padding + gy) * scale + py; + const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; + const auto where = std::string("'") + digits[i] + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy); + Require(channel(word, 24) == channel(clearWord, 24), where + " changed the alpha"); + if (set) { + Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: 0x" + std::to_string(word)); + continue; + } + for (const unsigned shift : {0u, 8u, 16u}) { + const int expected = static_cast(std::lround(channel(clearWord, shift) * 0.4)); + Require(std::abs(channel(word, shift) - expected) <= 1, where + " is not the panel over the frame: " + std::to_string(channel(word, shift)) + " for " + std::to_string(expected)); + } + } + } + } + } + } + std::printf("SDR overlay: panel %dx%d at %d,%d (scale %d), FPS digits drawn bit for bit\n", bounds.width, bounds.height, bounds.x, bounds.y, scale); +} + +void hdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { + const auto format = VK_FORMAT_A2B10G10R10_UNORM_PACK32; + VkFormatProperties properties{}; + context.formatProperties(context.physical, format, &properties); + if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT) == 0) { + std::printf("A2B10G10R10 cannot be blended into here; HDR10 overlay not exercised\n"); + return; + } + const VkExtent2D extent{1920, 1080}; + VkClearColorValue clear{}; + clear.float32[0] = 0.4f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.6f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, format, VK_COLOR_SPACE_HDR10_ST2084_EXT, extent, clear, content, OverlayCorner::BottomRight, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, format, VK_COLOR_SPACE_HDR10_ST2084_EXT, extent, clear, content, OverlayCorner::BottomRight, &bounds); + onlyInside(reference, drawn, extent, bounds, "HDR10 1920x1080"); + const auto scale = AgcDriver::OverlayScale(extent, content); + const auto& six = AgcDriver::OverlayGlyph('6'); + const auto column = static_cast(content.above.front().find('6')); + std::size_t checked = 0; + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::GlyphWidth; ++gx) { + if ((six[static_cast(gy)] & (0x10u >> gx)) == 0) continue; + const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + scale / 2; + const auto y = bounds.y + (OverlayMetrics::Padding + gy) * scale + scale / 2; + const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; + for (const unsigned shift : {0u, 10u, 20u}) { + const int code = static_cast((word >> shift) & 0x3ffu); + Require(std::abs(code - 594) <= 1, "HDR10 text is not at 203 nits: PQ code " + std::to_string(code) + " for 594"); + } + Require((word >> 30u) == (reference.front() >> 30u), "HDR10 text changed the alpha"); + ++checked; + } + } + Require(checked != 0, "no HDR10 text pixel was checked"); + std::printf("HDR10 overlay: text at PQ code 594 (203 nits), nothing changed outside the %dx%d panel\n", bounds.width, bounds.height); +} + +void cornerTest(const Context& context, PerformanceOverlay& overlay) { + const VkExtent2D extent{640, 360}; + VkClearColorValue clear{}; + clear.float32[1] = 1.0f; + clear.float32[3] = 1.0f; + const auto content = sixtyFps(); + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + for (const auto corner : {OverlayCorner::TopLeft, OverlayCorner::TopRight, OverlayCorner::BottomLeft, OverlayCorner::BottomRight}) { + OverlayRect bounds{}; + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, corner, &bounds); + const auto expected = AgcDriver::LayOutOverlay(content, extent, corner).bounds; + Require(bounds.x == expected.x && bounds.y == expected.y && bounds.width == expected.width && bounds.height == expected.height, "the drawn panel is not where the layout puts it"); + onlyInside(reference, drawn, extent, bounds, "corner " + std::to_string(static_cast(corner))); + } + std::printf("overlay corners: tl, tr, bl and br drawn where the layout puts them\n"); +} + +class Presenter { +public: + explicit Presenter(const Context& context) : context(context), pass(context, 1), overlay(context, 1), source(context, VK_FORMAT_B8G8R8A8_UNORM, {256, 144}, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT) { + Buffer upload(context, static_cast(256) * 144 * 4, VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::uint32_t seed = 0x2545f491u; + for (auto& byte : upload.Bytes()) { + seed = seed * 1664525u + 1013904223u; + byte = static_cast(seed >> 24u); + } + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + VkBufferImageCopy copy{}; + copy.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + copy.imageExtent = {256, 144, 1}; + context.Function("vkCmdCopyBufferToImage")(commands, upload.Handle(), source.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); + batch.SubmitAndWait(); + } + + std::vector Drawn(OverlayRect* bounds) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, Extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(Extent.width) * Extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const bool visible = PerformanceOverlay::Visible(); + pass.Bind(0, source.Handle(), 256, 144, Bgra8, AgcDriver::SdrToneMapLuminanceNits); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + pass.RecordPresent(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, visible ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + if (visible) *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sixtyFps(), OverlayCorner::TopLeft); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + pass.Release(0); + overlay.Release(0); + return wordsOf(readback, Extent); + } + + std::vector Blitted(OverlayRect* bounds) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, Extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + Buffer readback(context, static_cast(Extent.width) * Extent.height * 4, VK_BUFFER_USAGE_TRANSFER_DST_BIT); + const bool visible = PerformanceOverlay::Visible(); + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT); + VkClearColorValue black{}; + black.float32[3] = 1.0f; + const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + context.Function("vkCmdClearColorImage")(commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &range); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); + AgcDriver::PresentationScaler::RecordBlitFrom(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, 256, 144, VK_FILTER_LINEAR, destination.Handle(), Extent.width, Extent.height); + if (visible) { + *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sixtyFps(), OverlayCorner::TopLeft); + } else { + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); + } + transition(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_SHADER_READ_BIT); + readImage(context, commands, destination, readback); + batch.SubmitAndWait(); + } + overlay.Release(0); + return wordsOf(readback, Extent); + } + + static constexpr VkExtent2D Extent{400, 300}; + +private: + const Context& context; + PresentationPass pass; + PerformanceOverlay overlay; + Image source; +}; + +void disabledIsUnchangedTest(const Context& context) { + Require(!PerformanceOverlay::Visible(), "the performance overlay is shown without ANYPS5_OVERLAY=1"); + Presenter presenter(context); + OverlayRect unused{}; + const auto drawnBefore = presenter.Drawn(&unused); + const auto blittedBefore = presenter.Blitted(&unused); + PerformanceOverlay::Toggle(); + Require(PerformanceOverlay::Visible(), "F10 does not show the performance overlay"); + OverlayRect bounds{}; + onlyInside(drawnBefore, presenter.Drawn(&bounds), Presenter::Extent, bounds, "presentation pass with the overlay", false); + onlyInside(blittedBefore, presenter.Blitted(&bounds), Presenter::Extent, bounds, "blit with the overlay", false); + PerformanceOverlay::Toggle(); + Require(!PerformanceOverlay::Visible(), "F10 does not hide the performance overlay again"); + Require(presenter.Drawn(&unused) == drawnBefore, "the presentation pass output differs after the overlay was hidden"); + Require(presenter.Blitted(&unused) == blittedBefore, "the blit output differs after the overlay was hidden"); + std::printf("overlay hidden: presentation pass and blit output bit-identical before and after showing it\n"); +} + +void costReport(const Device& device, PerformanceOverlay& overlay) { + const auto& context = device.GetContext(); + if (device.TimestampBits() == 0) { + std::printf("the graphics queue has no timestamps; overlay cost not measured\n"); + return; + } + VkQueryPoolCreateInfo info{VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO}; + info.queryType = VK_QUERY_TYPE_TIMESTAMP; + info.queryCount = 2; + VkQueryPool queries = VK_NULL_HANDLE; + Check(context.Function("vkCreateQueryPool")(context.device, &info, nullptr, &queries), "vkCreateQueryPool overlay cost"); + for (const VkExtent2D extent : {VkExtent2D{1920, 1080}, VkExtent2D{3840, 2160}}) { + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT); + auto content = sixtyFps(); + content.below.push_back("GPU: 4.2 MS"); + content.below.push_back("DRIVER: 1.3 MS"); + std::vector gpuMs; + std::vector cpuMs; + for (int run = 0; run < 12; ++run) { + CommandBatch batch(context); + const auto commands = batch.Handle(); + context.Function("vkCmdResetQueryPool")(commands, queries, 0, 2); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, queries, 0); + const auto recordStart = std::chrono::steady_clock::now(); + static_cast(overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, content, OverlayCorner::TopLeft)); + cpuMs.push_back(std::chrono::duration(std::chrono::steady_clock::now() - recordStart).count()); + context.Function("vkCmdWriteTimestamp")(commands, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, queries, 1); + batch.SubmitAndWait(); + overlay.Release(0); + std::array stamps{}; + Check(context.Function("vkGetQueryPoolResults")(context.device, queries, 0, 2, sizeof(stamps), stamps.data(), sizeof(std::uint64_t), VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WAIT_BIT), "vkGetQueryPoolResults overlay cost"); + if (run >= 2) gpuMs.push_back(static_cast(stamps[1] - stamps[0]) * context.limits.timestampPeriod / 1e6); + } + std::sort(gpuMs.begin(), gpuMs.end()); + std::sort(cpuMs.begin(), cpuMs.end()); + std::printf("overlay cost on %s at %ux%u: GPU median %.3f ms (min %.3f, max %.3f), recording median %.3f ms\n", device.Name().c_str(), extent.width, extent.height, gpuMs[gpuMs.size() / 2], gpuMs.front(), gpuMs.back(), cpuMs[cpuMs.size() / 2]); + } + context.Function("vkDestroyQueryPool")(context.device, queries, nullptr); +} + +} + +int main() { + try { + setEnvironment("ANYPS5_OVERLAY", ""); + setEnvironment("ANYPS5_OVERLAY_POS", ""); + std::unique_ptr device; + try { + device = std::make_unique(); + } catch (const std::exception& error) { + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + std::printf("skipped, no usable Vulkan device: %s\n", error.what()); + return VulkanTestSkipped; + } + const auto& context = device->GetContext(); + { + PerformanceOverlay overlay(context, 1); + sdrOverlayTest(context, overlay); + hdrOverlayTest(context, overlay); + cornerTest(context, overlay); + costReport(*device, overlay); + } + disabledIsUnchangedTest(context); + std::cout << "performance overlay tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp new file mode 100644 index 0000000000..86ecedbb5b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -0,0 +1,238 @@ +#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::OverlayContent; +using AgcDriver::OverlayCorner; +using AgcDriver::OverlayMetrics; +using AgcDriver::OverlayRect; +using AgcDriver::OverlayStatistics; +using Clock = OverlayStatistics::Clock; +using namespace std::chrono_literals; + +constexpr Clock::time_point Start = Clock::time_point{} + std::chrono::hours(1); + +void require(bool condition, const std::string& reason) { + if (!condition) throw std::runtime_error(reason); +} + +bool near(double value, double expected, double tolerance = 1e-9) { + return std::abs(value - expected) <= tolerance; +} + +void fpsWindowTests() { + OverlayStatistics statistics; + require(statistics.Fps() == 0 && statistics.LastFrameMs() == 0 && statistics.FrameTimes().empty(), "a new overlay shows statistics"); + statistics.AddPresent(Start, 0, 0); + require(statistics.Fps() == 0, "one present reads as a frame rate"); + for (int i = 1; i <= 120; ++i) statistics.AddPresent(Start + i * 10ms, 0, 0); + require(near(statistics.Fps(), 100.0), "a present every 10 ms reads as " + std::to_string(statistics.Fps()) + " FPS, not 100"); + statistics.AddPresent(Start + 1700ms, 0, 0); + require(near(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51"); + statistics.AddPresent(Start + 3700ms, 0, 0); + require(statistics.Fps() == 0, "presents older than 1 s still count after a 2 s stall"); + statistics.AddPresent(Start + 3750ms, 0, 0); + require(near(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20"); + bool refused = false; + try { + statistics.AddPresent(Start + 3740ms, 0, 0); + } catch (const std::exception&) { + refused = true; + } + require(refused, "a present earlier than the previous one was accepted"); + statistics.Reset(); + require(statistics.Fps() == 0 && statistics.LastFrameMs() == 0 && statistics.FrameTimes().empty(), "a reset kept statistics"); +} + +void frameRingTests() { + OverlayStatistics statistics; + auto at = Start; + statistics.AddPresent(at, 0, 0); + for (int i = 1; i <= 4; ++i) { + at += std::chrono::milliseconds(i); + statistics.AddPresent(at, 0, 0); + } + const auto partial = statistics.FrameTimes(); + require(partial.size() == 4 && near(partial[0], 1) && near(partial[3], 4), "the frame times of a short run are not in present order"); + for (int i = 5; i <= 150; ++i) { + at += std::chrono::milliseconds(i); + statistics.AddPresent(at, 0, 0); + } + const auto times = statistics.FrameTimes(); + require(times.size() == OverlayStatistics::FrameHistory, "the ring holds " + std::to_string(times.size()) + " frames, not 120"); + for (std::size_t i = 0; i < times.size(); ++i) require(near(times[i], 31.0 + static_cast(i)), "frame " + std::to_string(i) + " of the ring is " + std::to_string(times[i]) + " ms"); + require(near(statistics.LastFrameMs(), 150), "the last frame is not the newest"); + const auto range = statistics.FrameTimeRange(); + require(near(range.minimum, 31) && near(range.maximum, 150) && near(range.average, 90.5), "the graph range of 31..150 ms reads as " + std::to_string(range.minimum) + "/" + std::to_string(range.average) + "/" + std::to_string(range.maximum)); +} + +void counterTests() { + OverlayStatistics statistics; + constexpr std::uint64_t Earlier = 1'000'000'000u; + for (std::uint64_t i = 0; i <= 60; ++i) statistics.AddPresent(Start + i * 16ms, Earlier + i * 4'000'000u, Earlier + i * 1'500'000u); + require(near(statistics.GpuMsPerFrame(), 4.0) && near(statistics.DriverMsPerFrame(), 1.5), "4 ms of GPU and 1.5 ms of driver time per frame read as " + std::to_string(statistics.GpuMsPerFrame()) + " and " + std::to_string(statistics.DriverMsPerFrame())); + statistics.AddPresent(Start + 61 * 16ms, 5, 5); + require(statistics.GpuMsPerFrame() == 0 && statistics.DriverMsPerFrame() == 0, "a counter that went back is read as time"); +} + +void formatTests() { + const std::pair cases[] = {{59.94, "59.9"}, {0, "0.0"}, {99.94, "99.9"}, {99.96, "100"}, {144.4, "144"}, {12345, "9999"}, {-3, "0.0"}, {std::nan(""), "0.0"}}; + for (const auto& [value, expected] : cases) require(AgcDriver::FormatOverlayValue(value) == expected, std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayValue(value) + "', not '" + expected + "'"); +} + +void settingsTests() { + const auto hidden = AgcDriver::ParseOverlaySettings(nullptr, nullptr); + require(!hidden.visible && hidden.corner == OverlayCorner::TopLeft, "the overlay is not off and top-left by default"); + require(!AgcDriver::ParseOverlaySettings("", "").visible && !AgcDriver::ParseOverlaySettings("0", nullptr).visible, "an empty or 0 ANYPS5_OVERLAY shows the overlay"); + require(AgcDriver::ParseOverlaySettings("1", nullptr).visible, "ANYPS5_OVERLAY=1 does not show the overlay"); + const std::pair corners[] = {{"tl", OverlayCorner::TopLeft}, {"tr", OverlayCorner::TopRight}, {"bl", OverlayCorner::BottomLeft}, {"br", OverlayCorner::BottomRight}}; + for (const auto& [name, corner] : corners) require(AgcDriver::ParseOverlaySettings("1", name).corner == corner, std::string("ANYPS5_OVERLAY_POS=") + name + " picks another corner"); + const auto refused = [](const char* enabled, const char* position, const char* variable) { + try { + static_cast(AgcDriver::ParseOverlaySettings(enabled, position)); + } catch (const std::exception& error) { + return std::string(error.what()).find(variable) != std::string::npos; + } + return false; + }; + require(refused("yes", nullptr, "ANYPS5_OVERLAY") && refused("2", nullptr, "ANYPS5_OVERLAY"), "an ANYPS5_OVERLAY value other than 1 or 0 was accepted or not named"); + require(refused("1", "center", "ANYPS5_OVERLAY_POS") && refused(nullptr, "TL", "ANYPS5_OVERLAY_POS"), "an unknown ANYPS5_OVERLAY_POS was accepted or not named"); +} + +void composeTests() { + OverlayStatistics statistics; + for (std::uint64_t i = 0; i <= 100; ++i) statistics.AddPresent(Start + i * 10ms, i * 2'000'000u, i * 500'000u); + const auto plain = AgcDriver::ComposeOverlay(statistics, false, false); + require(plain.above.size() == 2 && plain.above[0] == "FPS: 100" && plain.above[1] == "FRAME: 10.0 MS", "the frame lines read '" + plain.above[0] + "' and '" + plain.above[1] + "'"); + require(plain.below.size() == 1 && plain.below[0] == "MIN/AVG/MAX: 10.0/10.0/10.0", "the graph line reads '" + plain.below[0] + "'"); + require(plain.frameTimes.size() == 100, "the graph does not hold every frame time"); + const auto profiled = AgcDriver::ComposeOverlay(statistics, true, true); + require(profiled.below.size() == 3 && profiled.below[1] == "GPU: 2.0 MS" && profiled.below[2] == "DRIVER: 0.5 MS", "the profiled lines read '" + profiled.below[1] + "' and '" + profiled.below[2] + "'"); + require(AgcDriver::ComposeOverlay(statistics, true, false).below.size() == 2 && AgcDriver::ComposeOverlay(statistics, false, true).below[1].rfind("DRIVER", 0) == 0, "a line is shown without its source"); +} + +std::vector coverage(const AgcDriver::OverlayLayout& layout, std::uint32_t color) { + const auto& bounds = layout.bounds; + std::vector covered(static_cast(bounds.width) * static_cast(bounds.height), 0); + for (const auto& rect : layout.rects) { + if (rect.color != color) continue; + for (std::int32_t y = rect.y; y < rect.y + rect.height; ++y) { + for (std::int32_t x = rect.x; x < rect.x + rect.width; ++x) ++covered[static_cast(y - bounds.y) * static_cast(bounds.width) + static_cast(x - bounds.x)]; + } + } + return covered; +} + +void layoutTests() { + OverlayContent content{{"FPS: 60.0", "FRAME: 16.7 MS"}, {}, {"MIN/AVG/MAX: 16.7/16.7/16.7"}}; + for (int i = 0; i < 40; ++i) content.frameTimes.push_back(i == 39 ? 50.0 : 10.0); + const std::pair scales[] = {{{1280, 720}, 2}, {{1920, 1080}, 2}, {{2560, 1440}, 3}, {{3840, 2160}, 5}, {{320, 180}, 1}, {{2560, 720}, 2}}; + for (const auto& [extent, scale] : scales) require(AgcDriver::OverlayScale(extent, content) == scale, std::to_string(extent.width) + "x" + std::to_string(extent.height) + " draws the overlay at scale " + std::to_string(AgcDriver::OverlayScale(extent, content)) + ", not " + std::to_string(scale)); + + const VkExtent2D extent{1920, 1080}; + const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft); + const auto s = layout.scale; + const auto margin = OverlayMetrics::Margin * s; + require(layout.bounds.x == margin && layout.bounds.y == margin && layout.bounds.width == (OverlayMetrics::InnerWidth + 2 * OverlayMetrics::Padding) * s, "the top-left panel is not at the margin"); + require(layout.rects.front().x == layout.bounds.x && layout.rects.front().width == layout.bounds.width && layout.rects.front().color == static_cast(AgcDriver::OverlayColor::Background), "the background is not drawn first over the whole panel"); + for (const auto& rect : layout.rects) { + require(rect.width > 0 && rect.height > 0 && rect.x >= layout.bounds.x && rect.y >= layout.bounds.y && rect.x + rect.width <= layout.bounds.x + layout.bounds.width && rect.y + rect.height <= layout.bounds.y + layout.bounds.height, "a rectangle leaves the panel"); + } + + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + const auto lineTop = [&](std::size_t line) { return OverlayMetrics::Padding + static_cast(line) * OverlayMetrics::LinePitch; }; + const auto checkLine = [&](const std::string& line, std::int32_t top) { + for (std::size_t column = 0; column < line.size(); ++column) { + const auto& glyph = AgcDriver::OverlayGlyph(line[column]); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < s; ++py) { + for (std::int32_t px = 0; px < s; ++px) { + const auto x = (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance + gx) * s + px; + const auto y = (top + gy) * s + py; + require(text[static_cast(y) * static_cast(layout.bounds.width) + static_cast(x)] == (set ? 1 : 0), "'" + std::string(1, line[column]) + "' of '" + line + "' is drawn wrong at glyph pixel " + std::to_string(gx) + "," + std::to_string(gy)); + } + } + } + } + } + }; + checkLine(content.above[0], lineTop(0)); + checkLine(content.above[1], lineTop(1)); + checkLine(content.below[0], lineTop(2) + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap); + + std::vector bars; + for (const auto& rect : layout.rects) { + if (rect.color == static_cast(AgcDriver::OverlayColor::Graph)) bars.push_back(rect); + } + require(bars.size() == content.frameTimes.size(), "the graph has " + std::to_string(bars.size()) + " bars for 40 frames"); + const auto graphBottom = layout.bounds.y + (lineTop(2) + OverlayMetrics::GraphHeight) * s; + const auto full = OverlayMetrics::GraphHeight * s; + require(bars.back().x == layout.bounds.x + (OverlayMetrics::Padding + static_cast(OverlayStatistics::FrameHistory) - 1) * s, "the newest frame is not at the right end of the graph"); + for (std::size_t i = 0; i + 1 < bars.size(); ++i) require(bars[i + 1].x == bars[i].x + s && bars[i].width == s, "the graph bars are not side by side"); + require(bars.back().height == full && bars.back().y + bars.back().height == graphBottom, "the slowest frame does not fill the graph"); + require(bars.front().height == static_cast(std::lround(10.0 / 50.0 * full)) && bars.front().y + bars.front().height == graphBottom, "a 10 ms frame is not a fifth of a 50 ms one"); + OverlayContent steady = content; + std::fill(steady.frameTimes.begin(), steady.frameTimes.end(), 16.0); + for (const auto& rect : AgcDriver::LayOutOverlay(steady, extent, OverlayCorner::TopLeft).rects) { + if (rect.color == static_cast(AgcDriver::OverlayColor::Graph)) require(rect.height == full / 2, "steady frames do not reach half the graph"); + } + + const std::pair> corners[] = {{OverlayCorner::TopRight, {true, false}}, {OverlayCorner::BottomLeft, {false, true}}, {OverlayCorner::BottomRight, {true, true}}}; + for (const auto& [corner, sides] : corners) { + const auto placed = AgcDriver::LayOutOverlay(content, extent, corner).bounds; + const auto x = sides.first ? static_cast(extent.width) - margin - placed.width : margin; + const auto y = sides.second ? static_cast(extent.height) - margin - placed.height : margin; + require(placed.x == x && placed.y == y && placed.width == layout.bounds.width && placed.height == layout.bounds.height, "a corner places the panel elsewhere"); + } + OverlayContent profiled = content; + profiled.below.push_back("GPU: 2.0 MS"); + require(AgcDriver::LayOutOverlay(profiled, extent, OverlayCorner::TopLeft).bounds.height == layout.bounds.height + OverlayMetrics::LinePitch * s, "a profiled line does not add one line to the panel"); + + bool refused = false; + try { + static_cast(AgcDriver::OverlayGlyph('q')); + } catch (const std::exception&) { + refused = true; + } + require(refused, "a character without a glyph was drawn"); +} + +void paletteTests() { + const auto sdr = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); + require(sdr[1] == std::array{1.0f, 1.0f, 1.0f, 1.0f} && sdr[0][3] > 0.0f && sdr[0][3] < 1.0f, "the SDR palette is not opaque white text over a translucent panel"); + const auto srgb = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); + require(near(srgb[2][1], std::pow((0.8 + 0.055) / 1.055, 2.4), 1e-5) && srgb[1][0] == 1.0f, "an sRGB swapchain gets encoded colors"); + const auto hdr = AgcDriver::OverlayPalette(VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT); + for (std::size_t channel = 0; channel < 3; ++channel) require(near(hdr[1][channel], 0.5807, 0.0015), "HDR10 white text is not at 203 nits (PQ " + std::to_string(hdr[1][channel]) + ")"); + require(std::lround(hdr[0][0] * 1023.0f) == 0 && hdr[0][3] == sdr[0][3], "the HDR10 panel is not black at the same opacity"); +} + +} + +int main() { + try { + fpsWindowTests(); + frameRingTests(); + counterTests(); + formatTests(); + settingsTests(); + composeTests(); + layoutTests(); + paletteTests(); + std::cout << "performance overlay statistics, layout and palette tests passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From a41be04ed9829738b68217f3da0f56d9b30077fd Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:36:48 +0000 Subject: [PATCH 005/131] feat(videoout): toggle the performance overlay with F10 TogglePerformanceOverlay is bound to F10 by default and, like ToggleFullscreen, accepts only a keyboard key in anyps5-input.ini. --- .../prx/libScePad/include/InputMapping.hpp | 1 + .../prx/libScePad/include/KeyboardMouse.hpp | 1 + .../prx/libScePad/include/PadInputTypes.hpp | 3 ++- .../prx/libSceVideoOut/src/InputMapping.cpp | 6 +++++- .../prx/libSceVideoOut/src/KeyboardMouse.cpp | 2 ++ core/libs/prx/libSceVideoOut/src/PadInput.cpp | 2 ++ .../prx/libSceVideoOut/tests/InputMapping.cpp | 18 ++++++++++++++++++ .../prx/libSceVideoOut/tests/KeyboardMouse.cpp | 3 +++ 8 files changed, 34 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libScePad/include/InputMapping.hpp b/core/libs/prx/libScePad/include/InputMapping.hpp index a8914303d5..70e875fed8 100644 --- a/core/libs/prx/libScePad/include/InputMapping.hpp +++ b/core/libs/prx/libScePad/include/InputMapping.hpp @@ -35,6 +35,7 @@ struct InputConfiguration { inline constexpr std::array InputMapping{ InputBinding{SDL_SCANCODE_F11, MouseButton::None, InputControl::ToggleFullscreen}, + InputBinding{SDL_SCANCODE_F10, MouseButton::None, InputControl::TogglePerformanceOverlay}, InputBinding{SDL_SCANCODE_RETURN, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_SPACE, MouseButton::None, InputControl::Button, PadButton::Cross}, InputBinding{SDL_SCANCODE_ESCAPE, MouseButton::None, InputControl::Button, PadButton::Options}, diff --git a/core/libs/prx/libScePad/include/KeyboardMouse.hpp b/core/libs/prx/libScePad/include/KeyboardMouse.hpp index 68e501a384..6864ba9440 100644 --- a/core/libs/prx/libScePad/include/KeyboardMouse.hpp +++ b/core/libs/prx/libScePad/include/KeyboardMouse.hpp @@ -36,6 +36,7 @@ private: struct InputRequests { bool toggleFullscreen = false; + bool togglePerformanceOverlay = false; bool toggleMouse = false; bool releaseMouse = false; }; diff --git a/core/libs/prx/libScePad/include/PadInputTypes.hpp b/core/libs/prx/libScePad/include/PadInputTypes.hpp index 81842bae72..8f9db0ffb4 100644 --- a/core/libs/prx/libScePad/include/PadInputTypes.hpp +++ b/core/libs/prx/libScePad/include/PadInputTypes.hpp @@ -20,7 +20,8 @@ enum class InputControl { TouchLeft, TouchRight, ToggleMouse, - ToggleFullscreen + ToggleFullscreen, + TogglePerformanceOverlay }; enum class PadButton : std::uint32_t { diff --git a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp index d974db9e75..8662d18e0b 100644 --- a/core/libs/prx/libSceVideoOut/src/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/src/InputMapping.cpp @@ -52,7 +52,8 @@ constexpr auto actions = std::array{ InputAction{"TouchLeft", Pad::InputControl::TouchLeft, Pad::PadButton::None}, InputAction{"TouchRight", Pad::InputControl::TouchRight, Pad::PadButton::None}, InputAction{"ToggleMouse", Pad::InputControl::ToggleMouse, Pad::PadButton::None}, - InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None} + InputAction{"ToggleFullscreen", Pad::InputControl::ToggleFullscreen, Pad::PadButton::None}, + InputAction{"TogglePerformanceOverlay", Pad::InputControl::TogglePerformanceOverlay, Pad::PadButton::None} }; std::string upper(std::string_view value) { @@ -111,6 +112,9 @@ Pad::InputBinding parseBinding(const InputAction& action, std::string_view sourc if (action.control == Pad::InputControl::ToggleFullscreen) { throw std::runtime_error("ToggleFullscreen can only use a keyboard key"); } + if (action.control == Pad::InputControl::TogglePerformanceOverlay) { + throw std::runtime_error("TogglePerformanceOverlay can only use a keyboard key"); + } return {SDL_SCANCODE_UNKNOWN, parseMouseButton(value), action.control, action.button, 0}; } if (type == "WHEEL") { diff --git a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp index d8cf0e3d7c..07e0b26f6d 100644 --- a/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/src/KeyboardMouse.cpp @@ -146,6 +146,7 @@ Pad::InputRequests Pad::KeyboardMouse::HandleEvent(const SDL_Event& event, Clock : binding.mouseButton != MouseButton::None && binding.mouseButton == static_cast(event.button.button); if (!matches) continue; if (binding.control == InputControl::ToggleFullscreen && keyboard && down && !pressed[index]) requests.toggleFullscreen = true; + if (binding.control == InputControl::TogglePerformanceOverlay && keyboard && down && !pressed[index]) requests.togglePerformanceOverlay = true; if (binding.control == InputControl::ToggleMouse && down && !pressed[index]) requests.toggleMouse = true; pressed[index] = down; } @@ -177,6 +178,7 @@ PadInputState Pad::KeyboardMouse::Compose(const PadInputState& controller) const case InputControl::TouchRight: state.touchRight = true; break; case InputControl::ToggleMouse: break; case InputControl::ToggleFullscreen: break; + case InputControl::TogglePerformanceOverlay: break; } } for (std::size_t axis = 0; axis < state.sticks.size(); ++axis) { diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 06352b8775..b84286d6c7 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -10,6 +10,7 @@ #include "SDL.h" #include "prx/libSceVideoOut/include/PadInput.hpp" #include "prx/libSceVideoOut/include/DisplayWindow.hpp" +#include "prx/libSceAgcDriver/Execution/include/Presentation.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libc/include/General.hpp" @@ -226,6 +227,7 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { if (event.type == SDL_KEYDOWN && ImeCapturesKeyboard_nid_postfix()) return; const auto requests = keyboardMouse.HandleEvent(event, std::chrono::steady_clock::now()); if (requests.toggleFullscreen && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) window.ToggleFullscreen(); + if (requests.togglePerformanceOverlay && window.Handle() != nullptr && event.key.windowID == SDL_GetWindowID(window.Handle())) AgcDriverTogglePerformanceOverlay_nid_postfix(); if (requests.toggleMouse) setMouseMode(!keyboardMouse.MouseMode()); if (event.type != SDL_MOUSEMOTION) publish(); } diff --git a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp index daa2dad4d6..5789bb5b96 100644 --- a/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp +++ b/core/libs/prx/libSceVideoOut/tests/InputMapping.cpp @@ -75,6 +75,23 @@ void settingsReject() { require(rejects("MouseSensitivity = 1\nMouseSensitivity = 2\n"), "a setting given twice is rejected"); } +void performanceOverlayBinding() { + useConfig("Cross = KEY:F\n"); + bool onF10 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) onF10 = onF10 || (binding.control == Pad::InputControl::TogglePerformanceOverlay && binding.key == SDL_SCANCODE_F10); + require(onF10, "F10 toggles the performance overlay by default"); + useConfig("togglePerformanceOverlay = KEY:F9\n"); + bool onF9 = false; + bool stillOnF10 = false; + for (const auto& binding : Pad::LoadInputMapping().bindings) { + if (binding.control != Pad::InputControl::TogglePerformanceOverlay) continue; + onF9 = onF9 || binding.key == SDL_SCANCODE_F9; + stillOnF10 = stillOnF10 || binding.key == SDL_SCANCODE_F10; + } + require(onF9 && !stillOnF10, "the performance overlay toggle moves to another key"); + require(rejects("TogglePerformanceOverlay = MOUSE:Left\n"), "the performance overlay toggle refuses a mouse button"); +} + void mouseLookDefault() { useConfig("Cross = KEY:F\n"); const auto settings = Pad::LoadInputMapping().settings; @@ -138,6 +155,7 @@ int main() try { settingsDefault(); settingsParse(); settingsReject(); + performanceOverlayBinding(); mouseLookDefault(); mouseLookParse(); mouseLookReject(); diff --git a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp index 93715508cb..008c36e2e0 100644 --- a/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp +++ b/core/libs/prx/libSceVideoOut/tests/KeyboardMouse.cpp @@ -284,6 +284,9 @@ void testToggleRequests() { check(keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, true), now).toggleMouse, "middle click asks to toggle the mouse"); check(!keyboardMouse.HandleEvent(PadSimulation::ButtonEvent(SDL_BUTTON_MIDDLE, false), now).toggleMouse, "releasing it asks for nothing"); check(keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F11, true), now).toggleFullscreen, "F11 asks to toggle fullscreen"); + const auto overlay = keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, true), now); + check(overlay.togglePerformanceOverlay && !overlay.toggleFullscreen, "F10 asks to toggle the performance overlay"); + check(!keyboardMouse.HandleEvent(KeyEvent(SDL_SCANCODE_F10, false), now).togglePerformanceOverlay, "releasing F10 asks for nothing"); } } From 98b9b57ef3dc4e6ede3f18a6cd3220f5c40b2052 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:36:48 +0000 Subject: [PATCH 006/131] docs: describe the performance overlay --- docs/user/INPUT_MAPPING.md | 2 +- docs/user/USAGE.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/user/INPUT_MAPPING.md b/docs/user/INPUT_MAPPING.md index d61e36d70a..ad5b146e65 100644 --- a/docs/user/INPUT_MAPPING.md +++ b/docs/user/INPUT_MAPPING.md @@ -16,7 +16,7 @@ While a title has a mouse open (`sceMouseOpen`) and the window is focused, AnyPS Game-controller bindings are enabled automatically and are not affected by keyboard or mouse overrides in this file. -Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, and `ToggleFullscreen`. +Supported actions are `Cross`, `Circle`, `Triangle`, `Square`, `L1`, `R1`, `L2`, `R2`, `L3`, `R3`, `Options`, `Up`, `Right`, `Down`, `Left`, `LeftStickLeft`, `LeftStickRight`, `LeftStickUp`, `LeftStickDown`, `RightStickLeft`, `RightStickRight`, `RightStickUp`, `RightStickDown`, `TouchLeft`, `TouchRight`, `ToggleMouse`, `ToggleFullscreen` (F11), and `TogglePerformanceOverlay` (F10, the [performance overlay](USAGE.md#performance-overlay)). `ToggleFullscreen` and `TogglePerformanceOverlay` accept only keyboard keys. For example, this changes Cross to F or Space, moves the left stick to IJKL, uses the mouse buttons for Square and R2, and keeps all other built-in bindings: diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 1e736e2820..65ad40ed1d 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -165,6 +165,21 @@ Do not disable Dozen if it is the only Vulkan driver available; the application HDR output is available on Windows only. Titles see it when the monitor showing the window runs in HDR mode (Settings > Display > Use HDR), the window's Vulkan surface offers an HDR10 (`VK_COLOR_SPACE_HDR10_ST2084_EXT`) swapchain in a 10-bit format, the instance has `VK_EXT_swapchain_colorspace` and the device `VK_EXT_hdr_metadata`. The monitor's luminance comes from DXGI; a monitor that reports no peak luminance is taken as 1000 nits peak and full-frame, 0 nits black. On Linux and other hosts the output is always SDR, because the surface's formats do not show whether the display is in HDR. A title's BT.2100 PQ display buffers then reach the display unchanged; without HDR output they are converted to SDR. +### Performance overlay + +F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERLAY=1` shows it from the first frame. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. + +| Line | Meaning | +|------|---------| +| `FPS` | Frames presented per second, over the last second. | +| `FRAME` | Time between the last two presented frames, in ms. | +| graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | +| `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | +| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. | +| `DRIVER` | CPU time per frame over the last second, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | + +Times are shown with one decimal below 100 and as whole numbers above, up to 9999. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. + ### Storing GPU results at each flip GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. From 7126e05ca3b8f7fc7372b46aca5dcadfbac1f8bf Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:01:08 -0700 Subject: [PATCH 007/131] docs: say what the overlay's FPS and GPU lines count --- docs/user/USAGE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 65ad40ed1d..56f50fa8d8 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -171,11 +171,11 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | Line | Meaning | |------|---------| -| `FPS` | Frames presented per second, over the last second. | +| `FPS` | Frames presented per second, over the last second. Every present counts, including frames a title presents cleared, for example while it loads. | | `FRAME` | Time between the last two presented frames, in ms. | | graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | | `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | -| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. | +| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | | `DRIVER` | CPU time per frame over the last second, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | Times are shown with one decimal below 100 and as whole numbers above, up to 9999. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. From f12d2de271a58b84c4c3aa13bf742f972a395e46 Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:46:31 +0300 Subject: [PATCH 008/131] fix(libkernel): export _readv, _writev, _fcntl, _fstat, __sys_socketex OpenOrbis programs and modules import these PS4 libkernel names (the graphics, library and networking samples). The underscore names forward to readv, writev, fcntl and fstat, as _ioctl already does; _fcntl passes its argument on as a full 64-bit value, so the record lock commands keep their whole pointer. __sys_socketex creates a socket like socket(); its name argument is ignored, as sceNetSocket does. --- core/libs/prx/libkernel/File/src/Stdio.cpp | 12 ++++++++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 18 ++++++++++++++++++ core/libs/tests/GuestFileTimestamps.cpp | 6 ++++-- core/libs/tests/GuestFileVector.cpp | 7 +++++++ core/libs/tests/GuestKernelFcntl.cpp | 8 ++++++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 58f00fafaa..3e4b1da3b2 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -729,6 +729,10 @@ int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int d, FileStat* sb) { return 0; } +int64_t APS5_VABI _fstat_nid_postfix(int d, FileStat* sb) { + return fstat_nid_disambig1_nid_postfix(d, sb); +} + int APS5_VABI ftruncate_nid_postfix(int d, int64_t length) { if (length < 0 || GuestSockets::IsOpen(d)) { return PosixFailure(GUEST_EINVAL); @@ -1342,4 +1346,12 @@ int64_t APS5_VABI writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) return result < 0 ? PosixFailure(static_cast(result) & 0xffff) : result; } +int64_t APS5_VABI _readv_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { + return readv_nid_postfix(d, iov, iovcnt); +} + +int64_t APS5_VABI _writev_nid_postfix(int d, const KernelIovec* iov, int iovcnt) { + return writev_nid_postfix(d, iov, iovcnt); +} + } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 435d75c4d7..28771eb8c2 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -789,6 +789,20 @@ int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { socket->nonblocking = enabled != 0; return 0; } +int APS5_VABI _fcntl_nid_postfix(int descriptor, int command, ...) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + const auto argument = __builtin_va_arg(arguments, std::uint64_t); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, command); + const auto argument = va_arg(arguments, std::uint64_t); + va_end(arguments); +#endif + return fcntl_nid_postfix(descriptor, command, argument); +} int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length) { int netError = 0; @@ -838,6 +852,10 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) { return Fail(12); } } +int APS5_VABI __sys_socketex_nid_postfix(const char* name, int family, int type, int protocol) { + static_cast(name); + return socket_nid_postfix(family, type, protocol); +} int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* descriptors) { const bool nonblocking = (type & GuestNonBlocking) != 0; const int plainType = type & ~(GuestCloseOnExec | GuestNonBlocking); diff --git a/core/libs/tests/GuestFileTimestamps.cpp b/core/libs/tests/GuestFileTimestamps.cpp index 13c71a958d..b1e91115f8 100644 --- a/core/libs/tests/GuestFileTimestamps.cpp +++ b/core/libs/tests/GuestFileTimestamps.cpp @@ -19,6 +19,7 @@ int APS5_VABI stat_nid_postfix(const char*, FileStat*); int APS5_VABI lstat_nid_postfix(const char*, FileStat*); int APS5_VABI sceKernelFstat(int, FileStat*); std::int64_t APS5_VABI fstat_nid_disambig1_nid_postfix(int, FileStat*); +std::int64_t APS5_VABI _fstat_nid_postfix(int, FileStat*); int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); @@ -37,11 +38,12 @@ static bool Same(const KernelTimespec& actual, const KernelTimeval& expected) { } static void CheckTimes(const std::string& path, int fd, const KernelTimeval* expected) { - FileStat byPath{}, byFd{}, byKernel{}; + FileStat byPath{}, byFd{}, byKernel{}, byAlias{}; Require(stat_nid_postfix(path.c_str(), &byPath) == 0, "stat"); Require(fstat_nid_disambig1_nid_postfix(fd, &byFd) == 0, "fstat"); Require(sceKernelFstat(fd, &byKernel) == 0, "sceKernelFstat"); - for (const auto* status : {&byPath, &byFd, &byKernel}) { + Require(_fstat_nid_postfix(fd, &byAlias) == 0, "_fstat"); + for (const auto* status : {&byPath, &byFd, &byKernel, &byAlias}) { Require(Same(status->st_atim, expected[0]), "access time preserves microseconds"); Require(Same(status->st_mtim, expected[1]), "modification time preserves microseconds"); } diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp index 76f392261f..21d6249e87 100644 --- a/core/libs/tests/GuestFileVector.cpp +++ b/core/libs/tests/GuestFileVector.cpp @@ -36,6 +36,8 @@ int APS5_VABI sceKernelLseek(int, std::int64_t, int); std::int64_t APS5_VABI sceKernelReadv(int, const GuestIovec*, int); std::int64_t APS5_VABI readv_nid_postfix(int, const GuestIovec*, int); std::int64_t APS5_VABI writev_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _readv_nid_postfix(int, const GuestIovec*, int); +std::int64_t APS5_VABI _writev_nid_postfix(int, const GuestIovec*, int); int* APS5_VABI __error_nid_postfix(); std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int); std::int64_t APS5_VABI sceKernelPreadv(int, const GuestIovec*, int, std::int64_t); @@ -94,6 +96,8 @@ int main() { Require(sceKernelLseek(file, 0, 1) == 7); Require(readv_nid_postfix(file, reads, 2) == 3); Require(readv_nid_postfix(file, reads, 2) == 0); + Require(sceKernelLseek(file, 3, 0) == 3 && _readv_nid_postfix(file, reads, 2) == 7); + Require(std::memcmp(first, "345", 3) == 0 && std::memcmp(second, "6789", 4) == 0); Require(readv_nid_postfix(file, reads, -1) == -1 && *__error_nid_postfix() == 22); Require(writev_nid_postfix(file, nullptr, 1) == -1 && *__error_nid_postfix() == 14); @@ -118,6 +122,9 @@ int main() { Require(sceKernelLseek(file, 0, 1) == 13 && Contents(path) == "01ABCDE7CDEAB"); Require(sceKernelLseek(file, 8, 0) == 8 && writev_nid_postfix(file, writes, 2) == 5); Require(Contents(path) == "01ABCDE7ABCDE"); + Require(sceKernelLseek(file, 8, 0) == 8 && _writev_nid_postfix(file, swapped, 2) == 5); + Require(sceKernelLseek(file, 0, 1) == 13 && Contents(path) == "01ABCDE7CDEAB"); + Require(sceKernelLseek(file, 8, 0) == 8 && writev_nid_postfix(file, writes, 2) == 5); GuestIovec gapped[3] = {{first, 2}, {nullptr, 0}, {second, 3}}; Require(sceKernelPreadv(file, gapped, 3, 0) == 5); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 679a739b64..d53e717eaf 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -11,9 +11,11 @@ #include extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); +int APS5_VABI __sys_socketex_nid_postfix(const char*, int, int, int); int APS5_VABI close_nid_postfix(int); int APS5_VABI pipe_nid_postfix(int*); int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI _fcntl_nid_postfix(int, int, ...); int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); @@ -110,6 +112,8 @@ static void SocketsKeepTheirPath() { Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); Require(sceKernelFcntl(12345, GetFlags) == SceEbadf); + const int named = __sys_socketex_nid_postfix("probe", 2, 1, 0); + Require(named >= 0 && sceKernelFcntl(named, GetFlags) == ReadWrite && close_nid_postfix(named) == 0); } static void FileFlagsRoundTrip() { @@ -166,6 +170,10 @@ static void FileFlagsRoundTrip() { Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); GuestLock probe{0, 0, 0, 1, 0, 0}; Require(fcntl_nid_postfix(file, GetLock, &probe) == 0 && probe.type == 2); + static GuestLock imageProbe{0, 0, 0, 1, 0, 0}; + Require(_fcntl_nid_postfix(file, GetLock, &imageProbe) == 0 && imageProbe.type == 2); + Require(_fcntl_nid_postfix(file, SetDescriptorFlags, 1) == 0 && _fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(_fcntl_nid_postfix(file, SetDescriptorFlags, 0) == 0); lock.type = 2; Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); lock.type = 7; From 8997cac1b840998182bfc6afc32f70c91f5f3d56 Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:33:34 +0300 Subject: [PATCH 009/131] fix(relinker): stop module syscall scans at the PLT and skip the PS4 loader path OpenOrbis modules keep read-only data after their code, as executables do. The syscall scan of sce_module modules decoded it, and the module of the OpenOrbis library sample (libExample.prx) failed with "Forbidden syscall instruction". For modules with a PT_SCE_DYNLIBDATA segment, the scan now stops at the end of the PLT after the last unwind function, found by FindSceCodeEnd as for executables. The same modules begin their executable segment with the /libexec/ld-elf.so.1 string but have no PT_INTERP header, so code analysis decoded the string and failed with "Unsupported Windows guest TLS instruction (bytes: 64 2d 65 6c 66 2e)", which is the text "d-elf.". When no PT_INTERP header covers the start of a PS4 code segment, code analysis now starts after that exact string. --- core/relinker/CMakeLists.txt | 2 + .../src/analysis/CodeInstructionCollector.cpp | 3 + .../relinker/src/guest/GuestModuleBuilder.cpp | 11 +- .../tests/test_sce_module_code_extent.py | 160 ++++++++++++++++++ docs/dev/debt/relinker.md | 3 +- 5 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 core/relinker/relinker/tests/test_sce_module_code_extent.py diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index f8fd8039fe..477d4f9109 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -568,6 +568,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_code_extent.py $) add_test(NAME sce_relro COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_relro.py $) + add_test(NAME sce_module_code_extent COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_sce_module_code_extent.py $) add_test(NAME call_registry COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_call_registry.py $) add_test(NAME jmprel_table_bounds COMMAND ${Python3_EXECUTABLE} diff --git a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp index c5ffda7b73..0d4a06db1c 100644 --- a/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp +++ b/core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace Relinker { @@ -103,6 +104,8 @@ std::set CodeInstructionCollector::Collect(const std::ve auto begin = header.MappedAddress; for (const auto& interpreter : headers) if (interpreter.Type == 3 && interpreter.MappedAddress >= begin && interpreter.MappedAddress < end) begin = interpreter.MappedAddress + std::min(interpreter.FileSize, end - interpreter.MappedAddress); + const std::string_view loaderPath("/libexec/ld-elf.so.1", 21); + if (begin == header.MappedAddress && end - begin > loaderPath.size() && std::equal(loaderPath.begin(), loaderPath.end(), segment.begin())) begin += loaderPath.size(); header.Offset += begin - header.MappedAddress; header.MappedAddress = begin; header.FileSize = end - begin; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index d7598f0a26..6c9837a018 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -304,9 +306,14 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path image.Trampolines = std::move(converted.Trampolines); image.Bytes = std::move(converted.Bytes); } + const bool sceImage = std::any_of(image.Headers.begin(), image.Headers.end(), [](const Domain::ProgramHeader& header) { return header.Type == 0x61000000; }); + const auto unwindFunctions = sceImage ? UnusedNidFilter::ReadExceptionFunctions(image.Bytes, image.Headers, {}, {}) : std::vector{}; for (const auto& header : codeHeaders) { - const std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); - for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, header.FileSize, Io::Utf8Path(image.SourcePath))) { + std::vector code(image.Bytes.begin() + header.Offset, image.Bytes.begin() + header.Offset + header.FileSize); + if (sceImage) + if (const auto end = FindSceCodeEnd(code, header.MappedAddress, unwindFunctions, image.Got, image.Dynamic.RelaPltData.size())) + code.resize(*end - header.MappedAddress); + for (const auto offset : syscallScanner.ScanCodeSectionForSyscalls(code, header.MappedAddress, code.size(), Io::Utf8Path(image.SourcePath))) { if (offset > code.size() || code.size() - offset < 2u) throw Domain::RelinkerException("Syscall trap patch exceeds guest executable segment", header.MappedAddress + offset); image.Bytes[header.Offset + offset] = 0x0F; diff --git a/core/relinker/relinker/tests/test_sce_module_code_extent.py b/core/relinker/relinker/tests/test_sce_module_code_extent.py new file mode 100644 index 0000000000..28a19c721e --- /dev/null +++ b/core/relinker/relinker/tests/test_sce_module_code_extent.py @@ -0,0 +1,160 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_guest_symbol_names import importing_executable + +PT_LOAD = 1 +PT_DYNAMIC = 2 +PT_INTERP = 3 +PT_GNU_EH_FRAME = 0x6474E550 +PT_SCE_DYNLIBDATA = 0x61000000 +TEXT_OFFSET = 0x1000 +TEXT_SIZE = 0x1000 +DATA_OFFSET = 0x3000 +DATA_ADDRESS = 0x2000 +BLOCK_OFFSET = 0x3800 +IMAGE_SIZE = 0x4000 +INTERP = b"/libexec/ld-elf.so.1\x00" +FS_LOOKALIKE = b"\x64\x2d\x65\x6c\x66\x2e" +FUNCTION = 0x100 +FUNCTION_SIZE = 0x10 +PLT = 0x110 +RODATA = 0x140 +GOT = DATA_ADDRESS + 0x100 +EH_FRAME_HEADER = 0x800 +EH_FRAME = 0x840 + + +def fixture(interp_address=True, rodata_address=True, interp_header=True): + image = bytearray(IMAGE_SIZE) + base = BLOCK_OFFSET + strtab, symtab, jmprel = BLOCK_OFFSET, BLOCK_OFFSET + 0x20, BLOCK_OFFSET + 0x100 + tags = [ + (1, 1), + (0x61000035, strtab - base), (0x61000037, 16), + (0x61000039, symtab - base), (0x6100003B, 24), (0x6100003F, 48), + (0x6100002F, jmprel - base), (0x61000031, 0), (0x61000033, 24), + (0x61000027, GOT), (0x61000029, jmprel - base), (0x6100002B, 7), (0x6100002D, 24), + (0, 0), + ] + headers = [ + (PT_LOAD, 5, TEXT_OFFSET, 0, TEXT_SIZE, TEXT_SIZE, 0x1000), + (PT_LOAD, 6, DATA_OFFSET, DATA_ADDRESS, 0x400, 0x400, 0x1000), + (PT_DYNAMIC, 6, DATA_OFFSET, DATA_ADDRESS, len(tags) * 16, len(tags) * 16, 8), + (PT_SCE_DYNLIBDATA, 4, BLOCK_OFFSET, 0, 0x800, 0, 0x10), + (PT_INTERP if interp_header else 0, 4, TEXT_OFFSET, 0, len(INTERP), len(INTERP), 1), + (PT_GNU_EH_FRAME, 4, TEXT_OFFSET + EH_FRAME_HEADER, EH_FRAME_HEADER, 20, 20, 4), + ] + image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9) + struct.pack_into(" Date: Wed, 14 Oct 2026 11:14:37 -0700 Subject: [PATCH 010/131] docs: list the performance overlay with the diagnostics in USAGE Its section moves from after HDR output to before Windows crash diagnostics, next to the other switches for looking into a run. Other open changes add their own sections right after HDR output. --- docs/user/USAGE.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 56f50fa8d8..c169a8b495 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -165,21 +165,6 @@ Do not disable Dozen if it is the only Vulkan driver available; the application HDR output is available on Windows only. Titles see it when the monitor showing the window runs in HDR mode (Settings > Display > Use HDR), the window's Vulkan surface offers an HDR10 (`VK_COLOR_SPACE_HDR10_ST2084_EXT`) swapchain in a 10-bit format, the instance has `VK_EXT_swapchain_colorspace` and the device `VK_EXT_hdr_metadata`. The monitor's luminance comes from DXGI; a monitor that reports no peak luminance is taken as 1000 nits peak and full-frame, 0 nits black. On Linux and other hosts the output is always SDR, because the surface's formats do not show whether the display is in HDR. A title's BT.2100 PQ display buffers then reach the display unchanged; without HDR output they are converted to SDR. -### Performance overlay - -F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERLAY=1` shows it from the first frame. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. - -| Line | Meaning | -|------|---------| -| `FPS` | Frames presented per second, over the last second. Every present counts, including frames a title presents cleared, for example while it loads. | -| `FRAME` | Time between the last two presented frames, in ms. | -| graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | -| `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | -| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | -| `DRIVER` | CPU time per frame over the last second, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | - -Times are shown with one decimal below 100 and as whole numbers above, up to 9999. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. - ### Storing GPU results at each flip GPU results in storage images and render targets stay on the GPU until something reads their memory. A title that frees such an image after waiting for its GPU work and reuses the memory can have its new data overwritten when the results are stored later ([TechnicalDebt](../dev/TechnicalDebt.md)). `APS5_STORE_AT_FLIP=1` stores every pending result when a display buffer is flipped, at the cost of one write-back per pending image and frame. Any other value stops the title at its first flip. @@ -199,6 +184,21 @@ most called traced imports (1 calls in total): Limits are listed in the [libc](../dev/debt/libc.md) and [relinker](../dev/debt/relinker.md) technical debt. +### Performance overlay + +F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERLAY=1` shows it from the first frame. `ANYPS5_OVERLAY_POS` puts it in a corner of the window: `tl` (top left, the default), `tr`, `bl` or `br`. For either variable an empty value is the same as an unset one, and any other value stops the game at its first frame with an error that names the variable. The key can be moved with `TogglePerformanceOverlay` in the [input mapping](INPUT_MAPPING.md). The text scales with the window height, at a whole multiple of its 5x7 pixel font; on an HDR10 swapchain the overlay is drawn at 203 nits. + +| Line | Meaning | +|------|---------| +| `FPS` | Frames presented per second, over the last second. Every present counts, including frames a title presents cleared, for example while it loads. | +| `FRAME` | Time between the last two presented frames, in ms. | +| graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | +| `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | +| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | +| `DRIVER` | CPU time per frame over the last second, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | + +Times are shown with one decimal below 100 and as whole numbers above, up to 9999. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. + ### Windows crash diagnostics Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up to 64 bytes from each pointer found in the register memory dump. The report labels each block with its register, pointer offset and target address. Inaccessible, incomplete and executable targets are skipped. This additional dump is disabled when the variable is unset or empty. From 5b49ab30b28d0ea66fea24924a69fab1e8dec8e4 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:00:29 +0000 Subject: [PATCH 011/131] fix(agc): show the last frame's numbers in the performance overlay below 1 fps Below 1 fps no one-second window holds two presents, so the overlay showed FPS: 0.0, GPU: 0.0 MS and DRIVER: 0.0 MS, which is the common case for titles that run slowly today. When the last second holds fewer than two presents, FPS is now the last frame's rate, 1000 divided by its time in ms, and GPU and DRIVER are that frame's growth of each total. With two or more presents in the last second the numbers are the last second's as before, so the two modes meet at 1 fps. FPS below 1 is shown with two decimals: a frame every 2 s reads 0.50 and one every 12 s reads 0.08. Times keep their format and their 9999 cap, which USAGE.md now states for frames of 10 s or more. Nothing changes while the overlay is hidden. --- .../Execution/include/PerformanceOverlay.hpp | 3 + .../Execution/src/PerformanceOverlay.cpp | 24 +++++-- .../tests/PerformanceOverlayStatistics.cpp | 69 ++++++++++++++++++- docs/user/USAGE.md | 8 +-- 4 files changed, 94 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp index f99d27af9f..63f8cc5aad 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -72,6 +73,7 @@ private: double perFrameMs(std::uint64_t Sample::*counter) const; std::deque window; + std::optional previous; std::array frameTimes{}; std::size_t frameCount = 0; std::size_t frameNext = 0; @@ -115,6 +117,7 @@ struct OverlayMetrics { const std::array& OverlayGlyph(char character); std::string FormatOverlayValue(double value); +std::string FormatOverlayRate(double value); OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver); std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index f9218396a3..ff5e043363 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -113,6 +113,7 @@ void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanose frameTimes[frameNext] = std::chrono::duration(at - window.back().at).count(); frameNext = (frameNext + 1) % FrameHistory; frameCount = std::min(frameCount + 1, FrameHistory); + previous = window.back(); } window.push_back({at, gpuNanoseconds, driverNanoseconds}); while (at - window.front().at > Window) window.pop_front(); @@ -120,13 +121,17 @@ void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanose void OverlayStatistics::Reset() { window.clear(); + previous.reset(); frameTimes = {}; frameCount = 0; frameNext = 0; } double OverlayStatistics::Fps() const { - if (window.size() < 2) return 0; + if (window.size() < 2) { + const auto frameMs = LastFrameMs(); + return frameMs > 0 ? 1000.0 / frameMs : 0; + } const auto seconds = std::chrono::duration(window.back().at - window.front().at).count(); return seconds > 0 ? static_cast(window.size() - 1) / seconds : 0; } @@ -157,10 +162,12 @@ OverlayStatistics::Range OverlayStatistics::FrameTimeRange() const { } double OverlayStatistics::perFrameMs(std::uint64_t Sample::*counter) const { - if (window.size() < 2) return 0; - const auto first = window.front().*counter; + const bool windowed = window.size() >= 2; + if (!windowed && !previous) return 0; + const auto first = windowed ? window.front().*counter : (*previous).*counter; const auto last = window.back().*counter; - return last >= first ? static_cast(last - first) / 1e6 / static_cast(window.size() - 1) : 0; + const auto frames = windowed ? window.size() - 1 : std::size_t{1}; + return last >= first ? static_cast(last - first) / 1e6 / static_cast(frames) : 0; } double OverlayStatistics::GpuMsPerFrame() const { @@ -234,9 +241,16 @@ std::string FormatOverlayValue(double value) { return text; } +std::string FormatOverlayRate(double value) { + if (!std::isfinite(value) || value <= 0 || value >= 0.995) return FormatOverlayValue(value); + char text[16]; + std::snprintf(text, sizeof(text), "%.2f", value); + return text; +} + OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver) { OverlayContent content; - content.above.push_back("FPS: " + FormatOverlayValue(statistics.Fps())); + content.above.push_back("FPS: " + FormatOverlayRate(statistics.Fps())); content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS"); content.frameTimes = statistics.FrameTimes(); const auto range = statistics.FrameTimeRange(); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index 86ecedbb5b..504da82594 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -38,7 +38,7 @@ void fpsWindowTests() { statistics.AddPresent(Start + 1700ms, 0, 0); require(near(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51"); statistics.AddPresent(Start + 3700ms, 0, 0); - require(statistics.Fps() == 0, "presents older than 1 s still count after a 2 s stall"); + require(near(statistics.Fps(), 0.5), "after a 2 s stall the last second holds one present, read as " + std::to_string(statistics.Fps()) + " FPS, not the last frame's 0.5"); statistics.AddPresent(Start + 3750ms, 0, 0); require(near(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20"); bool refused = false; @@ -86,6 +86,72 @@ void counterTests() { void formatTests() { const std::pair cases[] = {{59.94, "59.9"}, {0, "0.0"}, {99.94, "99.9"}, {99.96, "100"}, {144.4, "144"}, {12345, "9999"}, {-3, "0.0"}, {std::nan(""), "0.0"}}; for (const auto& [value, expected] : cases) require(AgcDriver::FormatOverlayValue(value) == expected, std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayValue(value) + "', not '" + expected + "'"); + const std::pair rates[] = {{0.5, "0.50"}, {1000.0 / 12000.0, "0.08"}, {0.05, "0.05"}, {0.004, "0.00"}, {0.994, "0.99"}, {0.996, "1.0"}, {1.5, "1.5"}, {59.94, "59.9"}, {144.4, "144"}, {12345, "9999"}, {0, "0.0"}, {-1, "0.0"}, {std::nan(""), "0.0"}}; + for (const auto& [value, expected] : rates) require(AgcDriver::FormatOverlayRate(value) == expected, "a rate of " + std::to_string(value) + " is shown as '" + AgcDriver::FormatOverlayRate(value) + "', not '" + expected + "'"); +} + +void lowRateTests() { + OverlayStatistics slow; + constexpr std::uint64_t Gpu[] = {30'000'000u, 45'000'000u, 20'000'000u}; + constexpr std::uint64_t Driver[] = {12'000'000u, 20'000'000u, 8'000'000u}; + std::uint64_t gpuTotal = 0; + std::uint64_t driverTotal = 0; + slow.AddPresent(Start, 0, 0); + require(slow.Fps() == 0 && slow.GpuMsPerFrame() == 0 && slow.DriverMsPerFrame() == 0, "a single present reads as a frame"); + for (int i = 0; i < 3; ++i) { + gpuTotal += Gpu[i]; + driverTotal += Driver[i]; + slow.AddPresent(Start + (i + 1) * 2s, gpuTotal, driverTotal); + require(near(slow.Fps(), 0.5) && near(slow.LastFrameMs(), 2000), "a present every 2 s reads as " + std::to_string(slow.Fps()) + " FPS and " + std::to_string(slow.LastFrameMs()) + " ms, not 0.5 and 2000"); + require(near(slow.GpuMsPerFrame(), static_cast(Gpu[i]) / 1e6) && near(slow.DriverMsPerFrame(), static_cast(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own"); + } + const auto everyTwoSeconds = AgcDriver::ComposeOverlay(slow, true, true); + require(everyTwoSeconds.above[0] == "FPS: 0.50" && everyTwoSeconds.above[1] == "FRAME: 2000 MS", "a present every 2 s shows '" + everyTwoSeconds.above[0] + "' and '" + everyTwoSeconds.above[1] + "'"); + require(everyTwoSeconds.below[0] == "MIN/AVG/MAX: 2000/2000/2000" && everyTwoSeconds.below[1] == "GPU: 20.0 MS" && everyTwoSeconds.below[2] == "DRIVER: 8.0 MS", "a present every 2 s shows '" + everyTwoSeconds.below[0] + "', '" + everyTwoSeconds.below[1] + "' and '" + everyTwoSeconds.below[2] + "'"); + + OverlayStatistics glacial; + glacial.AddPresent(Start, 0, 0); + for (int i = 1; i <= 5; ++i) glacial.AddPresent(Start + i * 12s, static_cast(i) * 250'000'000u, static_cast(i) * 12'500'000'000u); + require(near(glacial.Fps(), 1000.0 / 12000.0) && near(glacial.LastFrameMs(), 12000) && near(glacial.GpuMsPerFrame(), 250) && near(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms"); + const auto everyTwelveSeconds = AgcDriver::ComposeOverlay(glacial, true, true); + require(everyTwelveSeconds.above[0] == "FPS: 0.08" && everyTwelveSeconds.above[1] == "FRAME: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.above[0] + "' and '" + everyTwelveSeconds.above[1] + "'"); + require(everyTwelveSeconds.below[0] == "MIN/AVG/MAX: 9999/9999/9999" && everyTwelveSeconds.below[1] == "GPU: 250 MS" && everyTwelveSeconds.below[2] == "DRIVER: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.below[0] + "', '" + everyTwelveSeconds.below[1] + "' and '" + everyTwelveSeconds.below[2] + "'"); + require(everyTwelveSeconds.frameTimes == std::vector(5, 12000.0), "the graph does not hold the five 12 s frames"); + const auto layout = AgcDriver::LayOutOverlay(everyTwelveSeconds, {1920, 1080}, OverlayCorner::TopLeft); + std::size_t bars = 0; + for (const auto& rect : layout.rects) { + if (rect.color != static_cast(AgcDriver::OverlayColor::Graph)) continue; + ++bars; + require(rect.height == OverlayMetrics::GraphHeight * layout.scale / 2, "steady 12 s frames do not reach half the graph"); + } + require(bars == 5, "the graph has " + std::to_string(bars) + " bars for five 12 s frames"); + + OverlayStatistics rising; + auto at = Start; + std::uint64_t total = 0; + rising.AddPresent(at, 0, 0); + const auto step = [&](Clock::duration interval, std::uint64_t gpuNanoseconds) { + at += interval; + total += gpuNanoseconds; + rising.AddPresent(at, total, 0); + }; + const auto check = [&](double fps, double gpuMs, const char* shown, const std::string& when) { + require(near(rising.Fps(), fps) && near(rising.GpuMsPerFrame(), gpuMs) && AgcDriver::FormatOverlayRate(rising.Fps()) == shown, when + " reads as " + std::to_string(rising.Fps()) + " FPS ('" + AgcDriver::FormatOverlayRate(rising.Fps()) + "') and " + std::to_string(rising.GpuMsPerFrame()) + " ms of GPU time, not " + std::to_string(fps) + " ('" + shown + "') and " + std::to_string(gpuMs)); + }; + step(2s, 40'000'000u); + check(0.5, 40, "0.50", "a 2 s frame"); + step(1250ms, 30'000'000u); + check(0.8, 30, "0.80", "a 1.25 s frame"); + step(1001ms, 20'000'000u); + check(1000.0 / 1001.0, 20, "1.0", "a 1.001 s frame, the last second holding one present"); + step(1000ms, 10'000'000u); + check(1.0, 10, "1.0", "a 1 s frame, the last second holding two presents"); + step(750ms, 6'000'000u); + check(1.0 / 0.75, 6, "1.3", "a 750 ms frame"); + for (int i = 0; i < 8; ++i) step(250ms, i % 2 == 0 ? 2'000'000u : 6'000'000u); + check(4.0, 4.0, "4.0", "four presents a second, alternating 2 and 6 ms of GPU time"); + step(3s, 9'000'000u); + check(1.0 / 3.0, 9, "0.33", "a 3 s frame after the fast ones"); } void settingsTests() { @@ -225,6 +291,7 @@ int main() { frameRingTests(); counterTests(); formatTests(); + lowRateTests(); settingsTests(); composeTests(); layoutTests(); diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index c169a8b495..8c561c4459 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -190,14 +190,14 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | Line | Meaning | |------|---------| -| `FPS` | Frames presented per second, over the last second. Every present counts, including frames a title presents cleared, for example while it loads. | +| `FPS` | Frames presented per second, over the last second. When the last second holds fewer than two presents (below 1 fps), the rate of the last frame instead, 1000 divided by its time in ms: a frame every 2 s shows `0.50`. Every present counts, including frames a title presents cleared, for example while it loads. | | `FRAME` | Time between the last two presented frames, in ms. | | graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | | `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | -| `GPU` | GPU time per frame over the last second, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | -| `DRIVER` | CPU time per frame over the last second, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | +| `GPU` | GPU time per frame over the last second, or of the last frame below 1 fps, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second, or by a frame below 1 fps. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | +| `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | -Times are shown with one decimal below 100 and as whole numbers above, up to 9999. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. +Times are shown with one decimal below 100 and as whole numbers above; anything above 9999 shows as 9999, so `FRAME` and `MIN/AVG/MAX` read 9999 for frames of 10 s or more. `FPS` has the same format, with two decimals below 1. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. ### Windows crash diagnostics From 060fe72e7da9bf9b8b317f8cda095431cdd5f6da Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:38 +0000 Subject: [PATCH 012/131] refactor(agc): make the largest device-local heap lookup public LargestDeviceLocalHeap picks the heap that the texture cache budgets read their VK_EXT_memory_budget numbers from. It moves out of the anonymous namespace of ShaderResources.cpp so the performance overlay can report the same heap. --- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/src/ShaderResources.cpp | 22 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 493436dcbc..ba7825f464 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -28,6 +28,7 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index e89fa2b31d..ea64c982c2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -474,14 +474,6 @@ std::shared_ptr sampledStorageSource(const Context& context, con std::uint64_t storageCacheBytes(); -std::optional largestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { - std::optional heap; - for (std::uint32_t i = 0; i < std::min(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++i) { - if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!heap || memory.memoryHeaps[i].size > memory.memoryHeaps[*heap].size)) heap = i; - } - return heap; -} - std::uint64_t forcedCacheBudget() { static const std::uint64_t forced = [] { const char* text = std::getenv("APS5_TEXTURE_CACHE_MIB"); @@ -508,7 +500,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { const auto storage = storageCacheBytes(); const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage); const bool fresh = cache.budgetDevice != context.physical; - if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { + if (const auto heap = LargestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { char was[48] = ""; if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(TextureCacheBudget(context.memory) >> 20u)); @@ -931,13 +923,21 @@ std::shared_ptr cachedStorageTexture(const Context& context, std } +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { + std::optional heap; + for (std::uint32_t i = 0; i < std::min(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++i) { + if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!heap || memory.memoryHeaps[i].size > memory.memoryHeaps[*heap].size)) heap = i; + } + return heap; +} + std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) { - const auto heap = largestDeviceLocalHeap(memory); + const auto heap = LargestDeviceLocalHeap(memory); return std::max(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u); } std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) { - const auto heap = largestDeviceLocalHeap(memory); + const auto heap = LargestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); const auto budget = reported->heapBudget[*heap]; const auto usage = reported->heapUsage[*heap]; From fe490d29de5861930d2c125db36a3c1a6f08628a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:38 +0000 Subject: [PATCH 013/131] feat(agc): show draws, pipelines, memory and resolution in the performance overlay The overlay gains five lines below the first ones, each shown only when its data exists: - DRAWS: draw and dispatch packets per frame over the last second. Each packet worker counts them in its own counter, one relaxed load, add and store per packet without a lock prefix, which the overlay sums once per shown frame. - SHADERS: pipelines created in the last second and in total, counted where a graphics pipeline is built for a draw state the cache had not seen and where a compute pipeline is created for a dispatch shader. - VRAM: usage and budget of the largest device-local heap from VK_EXT_memory_budget. - RAM: the process's private memory, PrivateUsage on Windows and RssAnon plus VmSwap from /proc/self/status on Linux. - RES: the display buffer's and the swapchain's size. VRAM and RAM are read at most once a second, and only while the overlay is shown; hidden, the only added work is the packet and pipeline increments. The font gains B, C, H, K, L, O, T and W. With every line the panel is 232 pixels tall at 1080p, a fifth of the window, so it keeps one mode. --- .../Execution/include/PerformanceOverlay.hpp | 64 ++++- .../src/Driver/Packets/PacketExecution.cpp | 3 + .../Execution/src/PerformanceOverlay.cpp | 219 ++++++++++++++++-- .../Execution/src/VulkanDevice.cpp | 3 +- .../libSceAgcDriver/Graphics/src/Pipeline.cpp | 2 + 5 files changed, 269 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp index 63f8cc5aad..653ff19e76 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp @@ -5,6 +5,7 @@ #include #include "prx/libSceAgcDriver/Graphics/include/Context.hpp" #include +#include #include #include #include @@ -36,12 +37,40 @@ class PerformanceCounters { public: static void AddGpuNanoseconds(std::uint64_t nanoseconds); static void AddDriverNanoseconds(std::uint64_t nanoseconds); + static void CountPipeline(); static std::uint64_t GpuNanoseconds(); static std::uint64_t DriverNanoseconds(); + static std::uint64_t Pipelines(); static bool GpuEnabled(); static bool DriverEnabled(); }; +class PacketCounters { +public: + struct Totals { + std::uint64_t draws = 0; + std::uint64_t dispatches = 0; + }; + + static PacketCounters& ThisThread(); + static Totals Sum(); + + void CountDraw() { draws.store(draws.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); } + void CountDispatch() { dispatches.store(dispatches.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed); } + +private: + alignas(64) std::atomic draws{0}; + std::atomic dispatches{0}; +}; + +struct OverlayCounters { + std::uint64_t gpuNanoseconds = 0; + std::uint64_t driverNanoseconds = 0; + std::uint64_t draws = 0; + std::uint64_t dispatches = 0; + std::uint64_t pipelines = 0; +}; + class OverlayStatistics { public: using Clock = std::chrono::steady_clock; @@ -55,6 +84,7 @@ public: }; void AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds); + void AddPresent(Clock::time_point at, const OverlayCounters& counters); void Reset(); double Fps() const; double LastFrameMs() const; @@ -62,15 +92,18 @@ public: Range FrameTimeRange() const; double GpuMsPerFrame() const; double DriverMsPerFrame() const; + double DrawsPerFrame() const; + double DispatchesPerFrame() const; + std::uint64_t PipelinesInWindow() const; + OverlayCounters Latest() const; private: struct Sample { Clock::time_point at; - std::uint64_t gpu = 0; - std::uint64_t driver = 0; + OverlayCounters counters; }; - double perFrameMs(std::uint64_t Sample::*counter) const; + double perFrame(std::uint64_t OverlayCounters::*counter) const; std::deque window; std::optional previous; @@ -96,6 +129,18 @@ struct OverlayContent { std::vector below; }; +struct OverlayMemory { + std::uint64_t used = 0; + std::uint64_t budget = 0; +}; + +struct OverlayDetails { + std::optional vram; + std::optional ram; + VkExtent2D display{}; + VkExtent2D window{}; +}; + struct OverlayLayout { OverlayRect bounds; std::vector rects; @@ -118,7 +163,13 @@ struct OverlayMetrics { const std::array& OverlayGlyph(char character); std::string FormatOverlayValue(double value); std::string FormatOverlayRate(double value); +std::string FormatOverlayCount(double value); +std::string FormatOverlayGigabytes(std::uint64_t bytes); OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver); +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details); +bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now); +std::optional ReadVramBudget(const Graphics::Context& context); +std::optional ReadProcessPrivateBytes(); std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content); OverlayLayout LayOutOverlay(const OverlayContent& content, VkExtent2D extent, OverlayCorner corner); std::array, OverlayColorCount> OverlayPalette(VkFormat format, VkColorSpaceKHR colorSpace); @@ -134,9 +185,9 @@ public: static void Toggle(); static OverlayCorner Corner(); - void NotePresent(OverlayStatistics::Clock::time_point at); - void NotePresent(OverlayStatistics::Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds); + void NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display); const OverlayStatistics& Statistics() const { return statistics; } + const OverlayDetails& Details() const { return details; } OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout); OverlayRect Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner); void Release(std::size_t slot) noexcept; @@ -152,6 +203,7 @@ private: VkPipeline pipeline(VkFormat format, VkRenderPass compatible); void releaseTransient(Slot& slot) noexcept; void release() noexcept; + void restartIfToggled(); Graphics::Context context; VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; @@ -161,6 +213,8 @@ private: std::map pipelines; std::vector slots; OverlayStatistics statistics; + OverlayDetails details; + std::optional sampledAt; std::uint64_t toggles = 0; }; 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 66f9d6d4b6..903d447aaf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -109,6 +109,7 @@ void Driver::execute(const Submission& submission) { thread_local PacketProfile* packetProfileSlot = nullptr; auto& packetProfile = ShaderRecompiler::ThreadOwned(packetProfileSlot); ++packetProfile.submissions; + auto& packetCounters = PacketCounters::ThisThread(); bumpEpoch(&EpochBumps::submissions); clearResolvedAhead(); @@ -247,6 +248,7 @@ void Driver::execute(const Submission& submission) { flipHold.Release(); submission.flips.at(cursor)->GpuReady(frame); } else if (opcode == 0x15 || opcode == 0x16) { + packetCounters.CountDispatch(); landPendingWaits(submission.queue); currentPacketOffset() = cursor; if (gateOpened()) { @@ -275,6 +277,7 @@ void Driver::execute(const Submission& submission) { } } } else if (drawPacket) { + packetCounters.CountDraw(); landPendingWaits(submission.queue); bool drawn = false; timed(&WorkerProfile::drawMs, [&] { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index ff5e043363..d1c24864a0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -5,14 +5,25 @@ #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayFrag_spv.h" #include "prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlayVert_spv.h" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include #include #include #include +#include #include #include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#endif namespace AgcDriver { namespace { @@ -27,6 +38,30 @@ struct OverlayParameters { std::atomic toggleCount{0}; std::atomic gpuTotal{0}; std::atomic driverTotal{0}; +std::atomic pipelineTotal{0}; + +struct PacketCounterRegistry { + std::mutex mutex; + std::deque counters; + std::vector unused; +}; + +PacketCounterRegistry& packetCounterRegistry() { + static auto* registry = new PacketCounterRegistry; + return *registry; +} + +struct PacketCounterLease { + PacketCounters* counters = nullptr; + ~PacketCounterLease() { + if (counters == nullptr) return; + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + registry.unused.push_back(counters); + } +}; + +constexpr double GigabyteBytes = 1024.0 * 1024.0 * 1024.0; const OverlaySettings& settings() { static const OverlaySettings parsed = ParseOverlaySettings(std::getenv("ANYPS5_OVERLAY"), std::getenv("ANYPS5_OVERLAY_POS")); @@ -90,6 +125,10 @@ void PerformanceCounters::AddDriverNanoseconds(std::uint64_t nanoseconds) { driverTotal.fetch_add(nanoseconds, std::memory_order_relaxed); } +void PerformanceCounters::CountPipeline() { + pipelineTotal.fetch_add(1, std::memory_order_relaxed); +} + std::uint64_t PerformanceCounters::GpuNanoseconds() { return gpuTotal.load(std::memory_order_relaxed); } @@ -98,6 +137,36 @@ std::uint64_t PerformanceCounters::DriverNanoseconds() { return driverTotal.load(std::memory_order_relaxed); } +std::uint64_t PerformanceCounters::Pipelines() { + return pipelineTotal.load(std::memory_order_relaxed); +} + +PacketCounters& PacketCounters::ThisThread() { + auto& lease = HostThreadLocal(); + if (lease.counters == nullptr) { + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + if (registry.unused.empty()) { + lease.counters = ®istry.counters.emplace_back(); + } else { + lease.counters = registry.unused.back(); + registry.unused.pop_back(); + } + } + return *lease.counters; +} + +PacketCounters::Totals PacketCounters::Sum() { + auto& registry = packetCounterRegistry(); + std::lock_guard lock(registry.mutex); + Totals totals; + for (const auto& counters : registry.counters) { + totals.draws += counters.draws.load(std::memory_order_relaxed); + totals.dispatches += counters.dispatches.load(std::memory_order_relaxed); + } + return totals; +} + bool PerformanceCounters::GpuEnabled() { return Graphics::Recorder::GpuTimingEnabled(); } @@ -108,6 +177,10 @@ bool PerformanceCounters::DriverEnabled() { } void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) { + AddPresent(at, OverlayCounters{gpuNanoseconds, driverNanoseconds}); +} + +void OverlayStatistics::AddPresent(Clock::time_point at, const OverlayCounters& counters) { if (!window.empty()) { Graphics::Require(at >= window.back().at, "presents reached the performance overlay out of order"); frameTimes[frameNext] = std::chrono::duration(at - window.back().at).count(); @@ -115,7 +188,7 @@ void OverlayStatistics::AddPresent(Clock::time_point at, std::uint64_t gpuNanose frameCount = std::min(frameCount + 1, FrameHistory); previous = window.back(); } - window.push_back({at, gpuNanoseconds, driverNanoseconds}); + window.push_back({at, counters}); while (at - window.front().at > Window) window.pop_front(); } @@ -161,21 +234,40 @@ OverlayStatistics::Range OverlayStatistics::FrameTimeRange() const { return range; } -double OverlayStatistics::perFrameMs(std::uint64_t Sample::*counter) const { +double OverlayStatistics::perFrame(std::uint64_t OverlayCounters::*counter) const { const bool windowed = window.size() >= 2; if (!windowed && !previous) return 0; - const auto first = windowed ? window.front().*counter : (*previous).*counter; - const auto last = window.back().*counter; + const auto first = windowed ? window.front().counters.*counter : previous->counters.*counter; + const auto last = window.back().counters.*counter; const auto frames = windowed ? window.size() - 1 : std::size_t{1}; - return last >= first ? static_cast(last - first) / 1e6 / static_cast(frames) : 0; + return last >= first ? static_cast(last - first) / static_cast(frames) : 0; } double OverlayStatistics::GpuMsPerFrame() const { - return perFrameMs(&Sample::gpu); + return perFrame(&OverlayCounters::gpuNanoseconds) / 1e6; } double OverlayStatistics::DriverMsPerFrame() const { - return perFrameMs(&Sample::driver); + return perFrame(&OverlayCounters::driverNanoseconds) / 1e6; +} + +double OverlayStatistics::DrawsPerFrame() const { + return perFrame(&OverlayCounters::draws); +} + +double OverlayStatistics::DispatchesPerFrame() const { + return perFrame(&OverlayCounters::dispatches); +} + +std::uint64_t OverlayStatistics::PipelinesInWindow() const { + if (window.empty()) return 0; + const auto first = window.front().counters.pipelines; + const auto last = window.back().counters.pipelines; + return last >= first ? last - first : 0; +} + +OverlayCounters OverlayStatistics::Latest() const { + return window.empty() ? OverlayCounters{} : window.back().counters; } const std::array& OverlayGlyph(char character) { @@ -197,18 +289,26 @@ const std::array& OverlayGlyph(char c static constexpr Glyph colon{0x00, 0x0c, 0x0c, 0x00, 0x0c, 0x0c, 0x00}; static constexpr Glyph slash{0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00}; static constexpr Glyph a{0x0e, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11}; + static constexpr Glyph b{0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e}; + static constexpr Glyph c{0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e}; static constexpr Glyph d{0x1c, 0x12, 0x11, 0x11, 0x11, 0x12, 0x1c}; static constexpr Glyph e{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f}; static constexpr Glyph f{0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10}; static constexpr Glyph g{0x0e, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0f}; + static constexpr Glyph h{0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11}; static constexpr Glyph i{0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e}; + static constexpr Glyph k{0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11}; + static constexpr Glyph l{0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f}; static constexpr Glyph m{0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11}; static constexpr Glyph n{0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11}; + static constexpr Glyph o{0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; static constexpr Glyph p{0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10}; static constexpr Glyph r{0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11}; static constexpr Glyph s{0x0f, 0x10, 0x10, 0x0e, 0x01, 0x01, 0x1e}; + static constexpr Glyph t{0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04}; static constexpr Glyph u{0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e}; static constexpr Glyph v{0x11, 0x11, 0x11, 0x11, 0x11, 0x0a, 0x04}; + static constexpr Glyph w{0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a}; static constexpr Glyph x{0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11}; if (character >= '0' && character <= '9') return digits[static_cast(character - '0')]; switch (character) { @@ -217,18 +317,26 @@ const std::array& OverlayGlyph(char c case ':': return colon; case '/': return slash; case 'A': return a; + case 'B': return b; + case 'C': return c; case 'D': return d; case 'E': return e; case 'F': return f; case 'G': return g; + case 'H': return h; case 'I': return i; + case 'K': return k; + case 'L': return l; case 'M': return m; case 'N': return n; + case 'O': return o; case 'P': return p; case 'R': return r; case 'S': return s; + case 'T': return t; case 'U': return u; case 'V': return v; + case 'W': return w; case 'X': return x; default: throw std::runtime_error(std::string("the performance overlay font has no glyph for '") + character + "'"); } @@ -248,7 +356,24 @@ std::string FormatOverlayRate(double value) { return text; } +std::string FormatOverlayCount(double value) { + const long long rounded = std::isfinite(value) && value > 0 ? std::llround(std::min(value, 1e12)) : 0; + char text[24]; + if (rounded < 10000) std::snprintf(text, sizeof(text), "%lld", rounded); + else if (rounded < 1000000) std::snprintf(text, sizeof(text), "%lldK", rounded / 1000); + else std::snprintf(text, sizeof(text), "%lldM", std::min(rounded / 1000000, 999)); + return text; +} + +std::string FormatOverlayGigabytes(std::uint64_t bytes) { + return FormatOverlayValue(static_cast(bytes) / GigabyteBytes); +} + OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver) { + return ComposeOverlay(statistics, gpu, driver, OverlayDetails{}); +} + +OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details) { OverlayContent content; content.above.push_back("FPS: " + FormatOverlayRate(statistics.Fps())); content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS"); @@ -257,9 +382,61 @@ OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, boo content.below.push_back("MIN/AVG/MAX: " + FormatOverlayValue(range.minimum) + "/" + FormatOverlayValue(range.average) + "/" + FormatOverlayValue(range.maximum)); if (gpu) content.below.push_back("GPU: " + FormatOverlayValue(statistics.GpuMsPerFrame()) + " MS"); if (driver) content.below.push_back("DRIVER: " + FormatOverlayValue(statistics.DriverMsPerFrame()) + " MS"); + const auto latest = statistics.Latest(); + if (latest.draws != 0 || latest.dispatches != 0) content.below.push_back("DRAWS: " + FormatOverlayCount(statistics.DrawsPerFrame()) + " DISPATCH: " + FormatOverlayCount(statistics.DispatchesPerFrame())); + if (latest.pipelines != 0) content.below.push_back("SHADERS: " + FormatOverlayCount(static_cast(statistics.PipelinesInWindow())) + " TOTAL: " + FormatOverlayCount(static_cast(latest.pipelines))); + if (details.vram) content.below.push_back("VRAM: " + FormatOverlayGigabytes(details.vram->used) + "/" + FormatOverlayGigabytes(details.vram->budget) + " GB"); + if (details.ram) content.below.push_back("RAM: " + FormatOverlayGigabytes(*details.ram) + " GB"); + if (details.display.width != 0 && details.display.height != 0 && details.window.width != 0 && details.window.height != 0) { + const auto size = [](VkExtent2D extent) { return std::to_string(extent.width) + "X" + std::to_string(extent.height); }; + for (const std::string_view separator : {" / ", "/"}) { + auto line = "RES: " + size(details.display) + std::string(separator) + size(details.window); + if (line.size() > static_cast(OverlayMetrics::Columns)) continue; + content.below.push_back(std::move(line)); + break; + } + } return content; } +bool OverlaySampleDue(std::optional sampledAt, OverlayStatistics::Clock::time_point now) { + return !sampledAt || now - *sampledAt >= std::chrono::seconds(1); +} + +std::optional ReadVramBudget(const Graphics::Context& context) { + if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return std::nullopt; + const auto heap = Graphics::LargestDeviceLocalHeap(context.memory); + if (!heap) return std::nullopt; + VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT}; + VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &reported}; + context.memoryProperties2(context.physical, &properties); + if (reported.heapBudget[*heap] == 0) return std::nullopt; + return OverlayMemory{reported.heapUsage[*heap], reported.heapBudget[*heap]}; +} + +std::optional ReadProcessPrivateBytes() { +#ifdef _WIN32 + PROCESS_MEMORY_COUNTERS_EX counters{}; + counters.cb = sizeof(counters); + if (K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast(&counters), sizeof(counters)) == 0) return std::nullopt; + return static_cast(counters.PrivateUsage); +#else + std::FILE* status = std::fopen("/proc/self/status", "r"); + if (status == nullptr) return std::nullopt; + std::optional anonymousKb; + std::uint64_t swapKb = 0; + char line[256]; + while (std::fgets(line, sizeof(line), status) != nullptr) { + unsigned long long value = 0; + if (std::sscanf(line, "RssAnon: %llu kB", &value) == 1) anonymousKb = value; + else if (std::sscanf(line, "VmSwap: %llu kB", &value) == 1) swapKb = value; + } + std::fclose(status); + if (!anonymousKb) return std::nullopt; + return (*anonymousKb + swapKb) * 1024u; +#endif +} + std::int32_t OverlayScale(VkExtent2D extent, const OverlayContent& content) { auto scale = std::max(1, static_cast((extent.height + 240) / 480)); while (scale > 1 && !fits(extent, content, scale)) --scale; @@ -418,17 +595,25 @@ OverlayCorner PerformanceOverlay::Corner() { return settings().corner; } -void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at) { - NotePresent(at, PerformanceCounters::GpuEnabled() ? PerformanceCounters::GpuNanoseconds() : 0, PerformanceCounters::DriverEnabled() ? PerformanceCounters::DriverNanoseconds() : 0); +void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, VkExtent2D display) { + restartIfToggled(); + if (OverlaySampleDue(sampledAt, at)) { + details.vram = ReadVramBudget(context); + details.ram = ReadProcessPrivateBytes(); + sampledAt = at; + } + details.display = display; + const auto packets = PacketCounters::Sum(); + statistics.AddPresent(at, OverlayCounters{PerformanceCounters::GpuEnabled() ? PerformanceCounters::GpuNanoseconds() : 0, PerformanceCounters::DriverEnabled() ? PerformanceCounters::DriverNanoseconds() : 0, packets.draws, packets.dispatches, PerformanceCounters::Pipelines()}); } -void PerformanceOverlay::NotePresent(OverlayStatistics::Clock::time_point at, std::uint64_t gpuNanoseconds, std::uint64_t driverNanoseconds) { +void PerformanceOverlay::restartIfToggled() { const auto current = toggleCount.load(std::memory_order_relaxed); - if (current != toggles) { - statistics.Reset(); - toggles = current; - } - statistics.AddPresent(at, gpuNanoseconds, driverNanoseconds); + if (current == toggles) return; + statistics.Reset(); + details = {}; + sampledAt.reset(); + toggles = current; } VkRenderPass PerformanceOverlay::renderPass(VkFormat format, VkImageLayout initialLayout, VkImageLayout finalLayout) { @@ -530,7 +715,9 @@ VkPipeline PerformanceOverlay::pipeline(VkFormat format, VkRenderPass compatible } OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t slot, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout) { - return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlay(statistics, PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled()), Corner()); + auto shown = details; + shown.window = extent; + return Record(commands, slot, destination, format, colorSpace, extent, initialLayout, finalLayout, ComposeOverlay(statistics, PerformanceCounters::GpuEnabled(), PerformanceCounters::DriverEnabled(), shown), Corner()); } OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t index, VkImage destination, VkFormat format, VkColorSpaceKHR colorSpace, VkExtent2D extent, VkImageLayout initialLayout, VkImageLayout finalLayout, const OverlayContent& content, OverlayCorner corner) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bc614c2e0a..59d5ce689e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -3079,7 +3079,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu } if (overlay) { if (!state->overlay) state->overlay = std::make_unique(graphicsContext(), state->presentSlots.size()); - state->overlay->NotePresent(std::chrono::steady_clock::now()); + state->overlay->NotePresent(std::chrono::steady_clock::now(), clearOnly ? VkExtent2D{} : VkExtent2D{width, height}); state->overlay->Record(commands, static_cast(&slot - state->presentSlots.data()), barrier.image, state->surfaceFormat, state->surfaceColorSpace, state->extent, drawn ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); } else if (!drawn) { barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; @@ -4540,6 +4540,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); objects->destroyModule(objects->device, objects->module, nullptr); objects->module = VK_NULL_HANDLE; + PerformanceCounters::CountPipeline(); Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); timing.Mark("pipeline_create"); if (pipelineKey != 0) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp index 87ae687aad..6096ed0aeb 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" +#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/PipelineLibrary.hpp" @@ -528,6 +529,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput for (const auto module : _modules) context.Function("vkDestroyShaderModule")(context.device, module, nullptr); _modules.clear(); timing.Mark("create"); + PerformanceCounters::CountPipeline(); LogPipelineStatistics_nid_no_patch(context, pipeline); } catch (...) { release(); From 6151c4f588073676280feee63c72d87b837599ac Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:38 +0000 Subject: [PATCH 014/131] test(agc): cover the performance overlay's new lines agc_performance_overlay_statistics checks the per-frame draw and dispatch averages, the pipelines of the last second, the per-worker packet counters across threads, the count and size formats, the order and omission of the new lines, the RES fallback for long sizes, the glyph bits of every line of the full panel and of the new letters, the once-a-second sampling and that touching 64 MiB raises the private memory. It also prints what a counted packet costs. agc_performance_overlay draws the full panel at 1920x1080 on lavapipe and compares every glyph pixel of every line, with nothing changed outside the panel; reads VRAM through VK_EXT_memory_budget; checks that a shown frame samples the memory and draws the VRAM, RAM and RES lines; and keeps the hidden presentation bit-identical with the full panel. --- .../tests/PerformanceOverlay.cpp | 184 +++++++++++--- .../tests/PerformanceOverlayStatistics.cpp | 232 ++++++++++++++++-- 2 files changed, 362 insertions(+), 54 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index b3e573052b..0992ffadeb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -61,6 +61,12 @@ public: while (family < count && (families[family].queueFlags & VK_QUEUE_GRAPHICS_BIT) == 0) ++family; Require(family < count, "no Vulkan graphics queue"); timestampBits = families[family].timestampValidBits; + const auto enumerateExtensions = function("vkEnumerateDeviceExtensionProperties"); + Check(enumerateExtensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties"); + std::vector extensions(count); + Check(enumerateExtensions(context.physical, nullptr, &count, extensions.data()), "vkEnumerateDeviceExtensionProperties"); + memoryBudget = std::any_of(extensions.begin(), extensions.end(), [](const VkExtensionProperties& extension) { return std::string(extension.extensionName) == VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; }); + const char* const budgetExtension = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; const float priority = 1; VkDeviceQueueCreateInfo queue{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; queue.queueFamilyIndex = family; @@ -69,7 +75,10 @@ public: VkDeviceCreateInfo device{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; device.queueCreateInfoCount = 1; device.pQueueCreateInfos = &queue; + device.enabledExtensionCount = memoryBudget ? 1 : 0; + device.ppEnabledExtensionNames = memoryBudget ? &budgetExtension : nullptr; Check(function("vkCreateDevice")(context.physical, &device, nullptr, &context.device), "vkCreateDevice"); + if (memoryBudget) context.memoryProperties2 = function("vkGetPhysicalDeviceMemoryProperties2"); context.deviceProc = function("vkGetDeviceProcAddr"); function("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory); VkPhysicalDeviceProperties properties{}; @@ -93,6 +102,7 @@ public: const Context& GetContext() const { return context; } const std::string& Name() const { return name; } std::uint32_t TimestampBits() const { return timestampBits; } + bool MemoryBudget() const { return memoryBudget; } private: template @@ -116,6 +126,7 @@ private: Context context{}; std::string name; std::uint32_t timestampBits = 0; + bool memoryBudget = false; }; class Image { @@ -247,6 +258,72 @@ int channel(std::uint32_t word, unsigned shift) { return static_cast((word >> shift) & 0xffu); } +void requireTextDrawn(const std::vector& drawn, VkExtent2D extent, const OverlayRect& bounds, std::int32_t scale, std::uint32_t frameWord, const std::string& line, std::int32_t row, std::size_t first = 0, std::size_t count = std::string::npos) { + const auto end = count == std::string::npos ? line.size() : std::min(line.size(), first + count); + for (std::size_t index = first; index < end; ++index) { + const auto& glyph = AgcDriver::OverlayGlyph(line[index]); + const auto column = static_cast(index); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < scale; ++py) { + for (std::int32_t px = 0; px < scale; ++px) { + const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + px; + const auto y = bounds.y + (row + gy) * scale + py; + const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; + const auto where = std::string("'") + line[index] + "' of '" + line + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy); + Require(channel(word, 24) == channel(frameWord, 24), where + " changed the alpha"); + if (set) { + Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: 0x" + std::to_string(word)); + continue; + } + for (const unsigned shift : {0u, 8u, 16u}) { + const int expected = static_cast(std::lround(channel(frameWord, shift) * 0.4)); + Require(std::abs(channel(word, shift) - expected) <= 1, where + " is not the panel over the frame: " + std::to_string(channel(word, shift)) + " for " + std::to_string(expected)); + } + } + } + } + } + } +} + +AgcDriver::OverlayDetails measuredDetails(const Context& context, VkExtent2D display, VkExtent2D window) { + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::ReadVramBudget(context); + details.ram = AgcDriver::ReadProcessPrivateBytes(); + details.display = display; + details.window = window; + return details; +} + +AgcDriver::OverlayDetails fixedDetails(VkExtent2D display, VkExtent2D window) { + constexpr double GiB = 1024.0 * 1024.0 * 1024.0; + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::OverlayMemory{static_cast(6.1 * GiB), static_cast(7.8 * GiB)}; + details.ram = static_cast(3.2 * GiB); + details.display = display; + details.window = window; + return details; +} + +OverlayContent fullPanel(const AgcDriver::OverlayDetails& details) { + AgcDriver::OverlayStatistics statistics; + const auto start = AgcDriver::OverlayStatistics::Clock::time_point{} + std::chrono::hours(1); + for (std::uint64_t i = 0; i <= 60; ++i) { + AgcDriver::OverlayCounters counters; + counters.gpuNanoseconds = i * 4'200'000u; + counters.driverNanoseconds = i * 1'300'000u; + counters.draws = i * 1240; + counters.dispatches = i * 85; + counters.pipelines = i < 50 ? 1500 : 1503; + statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, counters); + } + auto content = AgcDriver::ComposeOverlay(statistics, true, true, details); + Require(content.above.front() == "FPS: 60.0" && content.below.size() == 5u + (details.vram ? 1u : 0u) + (details.ram ? 1u : 0u) + (details.display.width != 0 ? 1u : 0u), "the full panel composes as '" + content.above.front() + "' with " + std::to_string(content.below.size()) + " lines below its graph"); + return content; +} + void sdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { const VkExtent2D extent{1280, 720}; VkClearColorValue clear{}; @@ -261,36 +338,10 @@ void sdrOverlayTest(const Context& context, PerformanceOverlay& overlay) { Require(bounds.x == AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft).bounds.x && bounds.width > 0 && bounds.height > 0, "the overlay drew another panel than its layout"); onlyInside(reference, drawn, extent, bounds, "SDR 1280x720"); const auto scale = AgcDriver::OverlayScale(extent, content); - const auto clearWord = reference.front(); const std::string digits = "60.0"; const auto firstColumn = content.above.front().find(digits); Require(firstColumn != std::string::npos, "the FPS line has no '60.0'"); - for (std::size_t i = 0; i < digits.size(); ++i) { - const auto& glyph = AgcDriver::OverlayGlyph(digits[i]); - const auto column = static_cast(firstColumn + i); - for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { - for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { - const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; - for (std::int32_t py = 0; py < scale; ++py) { - for (std::int32_t px = 0; px < scale; ++px) { - const auto x = bounds.x + (OverlayMetrics::Padding + column * OverlayMetrics::Advance + gx) * scale + px; - const auto y = bounds.y + (OverlayMetrics::Padding + gy) * scale + py; - const auto word = drawn[static_cast(y) * extent.width + static_cast(x)]; - const auto where = std::string("'") + digits[i] + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy); - Require(channel(word, 24) == channel(clearWord, 24), where + " changed the alpha"); - if (set) { - Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: 0x" + std::to_string(word)); - continue; - } - for (const unsigned shift : {0u, 8u, 16u}) { - const int expected = static_cast(std::lround(channel(clearWord, shift) * 0.4)); - Require(std::abs(channel(word, shift) - expected) <= 1, where + " is not the panel over the frame: " + std::to_string(channel(word, shift)) + " for " + std::to_string(expected)); - } - } - } - } - } - } + requireTextDrawn(drawn, extent, bounds, scale, reference.front(), content.above.front(), OverlayMetrics::Padding, firstColumn, digits.size()); std::printf("SDR overlay: panel %dx%d at %d,%d (scale %d), FPS digits drawn bit for bit\n", bounds.width, bounds.height, bounds.x, bounds.y, scale); } @@ -352,6 +403,69 @@ void cornerTest(const Context& context, PerformanceOverlay& overlay) { std::printf("overlay corners: tl, tr, bl and br drawn where the layout puts them\n"); } +void fullPanelTest(const Device& device, PerformanceOverlay& overlay) { + const auto& context = device.GetContext(); + const VkExtent2D extent{1920, 1080}; + auto details = measuredDetails(context, extent, extent); + Require(details.ram.has_value(), "the process's private memory cannot be read"); + Require(details.vram.has_value() == device.MemoryBudget(), device.MemoryBudget() ? "VK_EXT_memory_budget gives no device-local budget" : "VRAM is read without VK_EXT_memory_budget"); + auto withoutBudget = context; + withoutBudget.memoryProperties2 = nullptr; + Require(!AgcDriver::ReadVramBudget(withoutBudget).has_value(), "VRAM is read without vkGetPhysicalDeviceMemoryProperties2"); + VkClearColorValue clear{}; + clear.float32[0] = 0.75f; + clear.float32[1] = 0.5f; + clear.float32[2] = 0.25f; + clear.float32[3] = 1.0f; + const auto content = fullPanel(details); + OverlayRect bounds{}; + const auto reference = clearAndOverlay(context, nullptr, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, nullptr); + const auto drawn = clearAndOverlay(context, &overlay, VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, clear, content, OverlayCorner::TopLeft, &bounds); + const auto layout = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::TopLeft); + Require(bounds.x == layout.bounds.x && bounds.y == layout.bounds.y && bounds.width == layout.bounds.width && bounds.height == layout.bounds.height, "the full panel is not drawn where its layout puts it"); + onlyInside(reference, drawn, extent, bounds, "full panel 1920x1080"); + std::int32_t row = OverlayMetrics::Padding; + for (const auto& line : content.above) { + requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row); + row += OverlayMetrics::LinePitch; + } + row += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + requireTextDrawn(drawn, extent, bounds, layout.scale, reference.front(), line, row); + row += OverlayMetrics::LinePitch; + } + std::printf("full panel at 1920x1080: %dx%d at %d,%d (scale %d, %.1f%% of the height), every line drawn bit for bit, nothing changed outside it:\n", bounds.width, bounds.height, bounds.x, bounds.y, layout.scale, 100.0 * bounds.height / extent.height); + for (const auto& line : content.above) std::printf(" %s\n", line.c_str()); + std::printf(" (graph of %zu frame times)\n", content.frameTimes.size()); + for (const auto& line : content.below) std::printf(" %s\n", line.c_str()); +} + +void notePresentTest(const Device& device) { + const auto& context = device.GetContext(); + const VkExtent2D extent{1280, 720}; + PerformanceOverlay overlay(context, 1); + overlay.NotePresent(AgcDriver::OverlayStatistics::Clock::now(), VkExtent2D{640, 360}); + const auto& details = overlay.Details(); + Require(details.ram.has_value() && details.vram.has_value() == device.MemoryBudget() && details.display.width == 640 && details.display.height == 360, "a shown frame did not sample the memory or keep the display buffer size"); + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); + OverlayRect bounds{}; + { + CommandBatch batch(context); + const auto commands = batch.Handle(); + transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, VK_ACCESS_TRANSFER_WRITE_BIT); + bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); + batch.SubmitAndWait(); + } + overlay.Release(0); + auto shown = details; + shown.window = extent; + const auto expected = AgcDriver::ComposeOverlay(overlay.Statistics(), AgcDriver::PerformanceCounters::GpuEnabled(), AgcDriver::PerformanceCounters::DriverEnabled(), shown); + Require(expected.below.back() == "RES: 640X360 / 1280X720", "the RES line reads '" + expected.below.back() + "'"); + const auto layout = AgcDriver::LayOutOverlay(expected, extent, PerformanceOverlay::Corner()); + Require(bounds.height == layout.bounds.height && bounds.width == layout.bounds.width, "a shown frame's panel does not hold the sampled VRAM, RAM and RES lines"); + std::printf("shown frame: memory sampled, %zu lines below the graph, the last '%s'\n", expected.below.size(), expected.below.back().c_str()); +} + class Presenter { public: explicit Presenter(const Context& context) : context(context), pass(context, 1), overlay(context, 1), source(context, VK_FORMAT_B8G8R8A8_UNORM, {256, 144}, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT) { @@ -381,7 +495,7 @@ public: CommandBatch batch(context); const auto commands = batch.Handle(); pass.RecordPresent(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, visible ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); - if (visible) *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sixtyFps(), OverlayCorner::TopLeft); + if (visible) *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, fullPanel(fixedDetails({256, 144}, Extent)), OverlayCorner::TopLeft); readImage(context, commands, destination, readback); batch.SubmitAndWait(); } @@ -406,7 +520,7 @@ public: transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT); AgcDriver::PresentationScaler::RecordBlitFrom(context, commands, source.Handle(), VK_IMAGE_LAYOUT_GENERAL, 256, 144, VK_FILTER_LINEAR, destination.Handle(), Extent.width, Extent.height); if (visible) { - *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, sixtyFps(), OverlayCorner::TopLeft); + *bounds = overlay.Record(commands, 0, destination.Handle(), VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR, Extent, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, fullPanel(fixedDetails({256, 144}, Extent)), OverlayCorner::TopLeft); } else { transition(context, commands, destination.Handle(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); } @@ -456,11 +570,13 @@ void costReport(const Device& device, PerformanceOverlay& overlay) { info.queryCount = 2; VkQueryPool queries = VK_NULL_HANDLE; Check(context.Function("vkCreateQueryPool")(context.device, &info, nullptr, &queries), "vkCreateQueryPool overlay cost"); + const auto sampled = std::chrono::steady_clock::now(); + for (int run = 0; run < 100; ++run) static_cast(AgcDriver::ReadVramBudget(context)); + const auto vramUs = std::chrono::duration(std::chrono::steady_clock::now() - sampled).count() / 100; + std::printf("VRAM budget read in %.1f us (%s), once a second while the overlay is shown\n", vramUs, device.MemoryBudget() ? "VK_EXT_memory_budget" : "no VK_EXT_memory_budget"); for (const VkExtent2D extent : {VkExtent2D{1920, 1080}, VkExtent2D{3840, 2160}}) { Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT); - auto content = sixtyFps(); - content.below.push_back("GPU: 4.2 MS"); - content.below.push_back("DRIVER: 1.3 MS"); + const auto content = fullPanel(fixedDetails(extent, extent)); std::vector gpuMs; std::vector cpuMs; for (int run = 0; run < 12; ++run) { @@ -481,7 +597,7 @@ void costReport(const Device& device, PerformanceOverlay& overlay) { } std::sort(gpuMs.begin(), gpuMs.end()); std::sort(cpuMs.begin(), cpuMs.end()); - std::printf("overlay cost on %s at %ux%u: GPU median %.3f ms (min %.3f, max %.3f), recording median %.3f ms\n", device.Name().c_str(), extent.width, extent.height, gpuMs[gpuMs.size() / 2], gpuMs.front(), gpuMs.back(), cpuMs[cpuMs.size() / 2]); + std::printf("full panel cost on %s at %ux%u: GPU median %.3f ms (min %.3f, max %.3f), recording median %.3f ms\n", device.Name().c_str(), extent.width, extent.height, gpuMs[gpuMs.size() / 2], gpuMs.front(), gpuMs.back(), cpuMs[cpuMs.size() / 2]); } context.Function("vkDestroyQueryPool")(context.device, queries, nullptr); } @@ -506,8 +622,10 @@ int main() { sdrOverlayTest(context, overlay); hdrOverlayTest(context, overlay); cornerTest(context, overlay); + fullPanelTest(*device, overlay); costReport(*device, overlay); } + notePresentTest(*device); disabledIsUnchangedTest(context); std::cout << "performance overlay tests passed\n"; return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index 504da82594..583565c9f2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -3,9 +3,14 @@ #include #include #include +#include +#include #include +#include +#include #include #include +#include #include namespace { @@ -197,6 +202,25 @@ std::vector coverage(const AgcDriver::OverlayLayout& layout, std:: return covered; } +void requireLineDrawn(const AgcDriver::OverlayLayout& layout, const std::vector& text, const std::string& line, std::int32_t top) { + const auto s = layout.scale; + for (std::size_t column = 0; column < line.size(); ++column) { + const auto& glyph = AgcDriver::OverlayGlyph(line[column]); + for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { + for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { + const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; + for (std::int32_t py = 0; py < s; ++py) { + for (std::int32_t px = 0; px < s; ++px) { + const auto x = (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance + gx) * s + px; + const auto y = (top + gy) * s + py; + require(text[static_cast(y) * static_cast(layout.bounds.width) + static_cast(x)] == (set ? 1 : 0), "'" + std::string(1, line[column]) + "' of '" + line + "' is drawn wrong at glyph pixel " + std::to_string(gx) + "," + std::to_string(gy)); + } + } + } + } + } +} + void layoutTests() { OverlayContent content{{"FPS: 60.0", "FRAME: 16.7 MS"}, {}, {"MIN/AVG/MAX: 16.7/16.7/16.7"}}; for (int i = 0; i < 40; ++i) content.frameTimes.push_back(i == 39 ? 50.0 : 10.0); @@ -215,26 +239,9 @@ void layoutTests() { const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); const auto lineTop = [&](std::size_t line) { return OverlayMetrics::Padding + static_cast(line) * OverlayMetrics::LinePitch; }; - const auto checkLine = [&](const std::string& line, std::int32_t top) { - for (std::size_t column = 0; column < line.size(); ++column) { - const auto& glyph = AgcDriver::OverlayGlyph(line[column]); - for (std::int32_t gy = 0; gy < OverlayMetrics::GlyphHeight; ++gy) { - for (std::int32_t gx = 0; gx < OverlayMetrics::Advance; ++gx) { - const bool set = gx < OverlayMetrics::GlyphWidth && (glyph[static_cast(gy)] & (0x10u >> gx)) != 0; - for (std::int32_t py = 0; py < s; ++py) { - for (std::int32_t px = 0; px < s; ++px) { - const auto x = (OverlayMetrics::Padding + static_cast(column) * OverlayMetrics::Advance + gx) * s + px; - const auto y = (top + gy) * s + py; - require(text[static_cast(y) * static_cast(layout.bounds.width) + static_cast(x)] == (set ? 1 : 0), "'" + std::string(1, line[column]) + "' of '" + line + "' is drawn wrong at glyph pixel " + std::to_string(gx) + "," + std::to_string(gy)); - } - } - } - } - } - }; - checkLine(content.above[0], lineTop(0)); - checkLine(content.above[1], lineTop(1)); - checkLine(content.below[0], lineTop(2) + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap); + requireLineDrawn(layout, text, content.above[0], lineTop(0)); + requireLineDrawn(layout, text, content.above[1], lineTop(1)); + requireLineDrawn(layout, text, content.below[0], lineTop(2) + OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap); std::vector bars; for (const auto& rect : layout.rects) { @@ -273,6 +280,182 @@ void layoutTests() { require(refused, "a character without a glyph was drawn"); } +constexpr std::uint64_t GiB = 1ull << 30u; + +OverlayStatistics countedStatistics() { + OverlayStatistics statistics; + for (std::uint64_t i = 0; i <= 60; ++i) { + AgcDriver::OverlayCounters counters; + counters.gpuNanoseconds = i * 4'000'000u; + counters.driverNanoseconds = i * 1'500'000u; + counters.draws = 5000 + i * 1240; + counters.dispatches = 70 + i * 85; + counters.pipelines = i < 50 ? 1500 : 1503; + statistics.AddPresent(Start + i * 16ms, counters); + } + return statistics; +} + +AgcDriver::OverlayDetails fullDetails() { + AgcDriver::OverlayDetails details; + details.vram = AgcDriver::OverlayMemory{static_cast(6.1 * GiB), static_cast(7.8 * GiB)}; + details.ram = static_cast(3.2 * GiB); + details.display = {1920, 1080}; + details.window = {2560, 1440}; + return details; +} + +void packetStatisticsTests() { + const auto statistics = countedStatistics(); + require(near(statistics.DrawsPerFrame(), 1240) && near(statistics.DispatchesPerFrame(), 85), "1240 draws and 85 dispatches per frame read as " + std::to_string(statistics.DrawsPerFrame()) + " and " + std::to_string(statistics.DispatchesPerFrame())); + require(statistics.PipelinesInWindow() == 3 && statistics.Latest().pipelines == 1503, "3 pipelines created within the last second of 1503 read as " + std::to_string(statistics.PipelinesInWindow()) + " of " + std::to_string(statistics.Latest().pipelines)); + require(near(statistics.GpuMsPerFrame(), 4.0) && near(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters"); + auto stalled = statistics; + auto counters = stalled.Latest(); + counters.pipelines += 7; + stalled.AddPresent(Start + 3000ms, counters); + require(stalled.DrawsPerFrame() == 0 && stalled.PipelinesInWindow() == 0 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as packets or pipelines of that second"); + auto restarted = statistics; + counters = restarted.Latest(); + counters.draws = 3; + restarted.AddPresent(Start + 61 * 16ms, counters); + require(restarted.DrawsPerFrame() == 0, "a draw counter that went back is read as draws"); + OverlayStatistics empty; + require(empty.PipelinesInWindow() == 0 && empty.Latest().draws == 0, "an overlay without presents shows packet counts"); +} + +void packetCounterTests() { + const auto before = AgcDriver::PacketCounters::Sum(); + auto& counters = AgcDriver::PacketCounters::ThisThread(); + require(&counters == &AgcDriver::PacketCounters::ThisThread(), "one thread got two packet counters"); + for (int i = 0; i < 1240; ++i) counters.CountDraw(); + for (int i = 0; i < 85; ++i) counters.CountDispatch(); + bool separate = false; + std::thread worker([&] { + auto& own = AgcDriver::PacketCounters::ThisThread(); + separate = &own != &counters; + for (int i = 0; i < 1000; ++i) own.CountDraw(); + own.CountDispatch(); + }); + worker.join(); + require(separate, "two workers share packet counters"); + const auto after = AgcDriver::PacketCounters::Sum(); + require(after.draws - before.draws == 2240 && after.dispatches - before.dispatches == 86, "two workers' 2240 draws and 86 dispatches sum to " + std::to_string(after.draws - before.draws) + " and " + std::to_string(after.dispatches - before.dispatches)); + std::thread next([] { AgcDriver::PacketCounters::ThisThread().CountDraw(); }); + next.join(); + require(AgcDriver::PacketCounters::Sum().draws == after.draws + 1, "the counts of a worker that ended were lost, or a new worker restarted them"); +} + +void countFormatTests() { + const std::pair counts[] = {{0, "0"}, {0.4, "0"}, {0.5, "1"}, {85, "85"}, {1239.6, "1240"}, {9999, "9999"}, {9999.5, "10K"}, {12345, "12K"}, {999999, "999K"}, {1e6, "1M"}, {2.5e9, "999M"}, {-3, "0"}, {std::nan(""), "0"}, {INFINITY, "0"}}; + for (const auto& [value, expected] : counts) require(AgcDriver::FormatOverlayCount(value) == expected, std::to_string(value) + " is counted as '" + AgcDriver::FormatOverlayCount(value) + "', not '" + expected + "'"); + const std::pair sizes[] = {{0, "0.0"}, {static_cast(6.1 * GiB), "6.1"}, {static_cast(7.8 * GiB), "7.8"}, {GiB / 2, "0.5"}, {1536 * GiB, "1536"}, {20000 * GiB, "9999"}}; + for (const auto& [bytes, expected] : sizes) require(AgcDriver::FormatOverlayGigabytes(bytes) == expected, std::to_string(bytes) + " bytes are shown as '" + AgcDriver::FormatOverlayGigabytes(bytes) + "' GB, not '" + expected + "'"); +} + +void detailComposeTests() { + const auto statistics = countedStatistics(); + const auto full = AgcDriver::ComposeOverlay(statistics, true, true, fullDetails()); + const std::vector expected{"MIN/AVG/MAX: 16.0/16.0/16.0", "GPU: 4.0 MS", "DRIVER: 1.5 MS", "DRAWS: 1240 DISPATCH: 85", "SHADERS: 3 TOTAL: 1503", "VRAM: 6.1/7.8 GB", "RAM: 3.2 GB", "RES: 1920X1080 / 2560X1440"}; + require(full.below == expected, "the full panel's lines below the graph read '" + (full.below.empty() ? std::string() : full.below.back()) + "' last, of " + std::to_string(full.below.size())); + for (const auto& line : full.below) require(line.size() <= static_cast(OverlayMetrics::Columns), "'" + line + "' is wider than the panel"); + + OverlayStatistics uncounted; + for (int i = 0; i <= 60; ++i) uncounted.AddPresent(Start + i * 16ms, 0, 0); + const auto plain = AgcDriver::ComposeOverlay(uncounted, false, false); + const auto none = AgcDriver::ComposeOverlay(uncounted, false, false, AgcDriver::OverlayDetails{}); + require(none.above == plain.above && none.below == plain.below && none.below.size() == 1, "a panel without packets, pipelines, memory or a display buffer shows more than the first lines"); + + auto details = fullDetails(); + details.vram.reset(); + details.ram.reset(); + details.display = {}; + const auto partial = AgcDriver::ComposeOverlay(statistics, false, false, details); + require(partial.below.size() == 3 && partial.below.back().rfind("SHADERS", 0) == 0, "a line is shown without its data: the last line is '" + partial.below.back() + "'"); + + details = fullDetails(); + details.display = {3840, 2160}; + details.window = {16384, 16384}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RES: 3840X2160/16384X16384", "a long RES line is not written without spaces"); + details.display = {12345, 12345}; + details.window = {12345, 12345}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RAM: 3.2 GB", "a RES line wider than the panel is shown"); + details = fullDetails(); + details.window = {}; + require(AgcDriver::ComposeOverlay(uncounted, false, false, details).below.back() == "RAM: 3.2 GB", "RES is shown without a window size"); + auto large = countedStatistics(); + auto counters = large.Latest(); + counters.draws += 12345 * 2; + counters.pipelines = 123456; + large.AddPresent(Start + 62 * 16ms, counters); + const auto busy = AgcDriver::ComposeOverlay(large, false, false); + require(busy.below.size() == 3 && busy.below[2] == "SHADERS: 121K TOTAL: 123K", "large counts are not shortened: '" + busy.below.back() + "'"); +} + +void fullPanelLayoutTests() { + auto content = AgcDriver::ComposeOverlay(countedStatistics(), true, true, fullDetails()); + content.below[3] = "DRAWS: 9999 DISPATCH: 12K"; + content.below[4] = "SHADERS: 999M TOTAL: 9999"; + const VkExtent2D hd{1920, 1080}; + const auto layout = AgcDriver::LayOutOverlay(content, hd, OverlayCorner::TopLeft); + require(layout.scale == 2 && layout.bounds.height * 3 <= static_cast(hd.height), "the full panel at 1080p is " + std::to_string(layout.bounds.height) + " pixels tall at scale " + std::to_string(layout.scale) + ", more than a third of the window"); + const auto text = coverage(layout, static_cast(AgcDriver::OverlayColor::Text)); + std::int32_t top = OverlayMetrics::Padding; + for (const auto& line : content.above) { + requireLineDrawn(layout, text, line, top); + top += OverlayMetrics::LinePitch; + } + top += OverlayMetrics::GraphHeight + OverlayMetrics::GraphGap; + for (const auto& line : content.below) { + requireLineDrawn(layout, text, line, top); + top += OverlayMetrics::LinePitch; + } + require(layout.bounds.height == (top - OverlayMetrics::LinePitch + OverlayMetrics::GlyphHeight + OverlayMetrics::Padding) * layout.scale, "the panel does not end one padding below its last line"); + const std::pair scales[] = {{{1280, 720}, 2}, {{2560, 1440}, 3}, {{3840, 2160}, 5}, {{320, 180}, 1}}; + for (const auto& [extent, scale] : scales) { + const auto placed = AgcDriver::LayOutOverlay(content, extent, OverlayCorner::BottomRight); + require(placed.scale == scale && placed.bounds.x >= 0 && placed.bounds.y >= 0 && placed.bounds.y + placed.bounds.height <= static_cast(extent.height), "the full panel does not fit " + std::to_string(extent.width) + "x" + std::to_string(extent.height) + " at scale " + std::to_string(scale)); + } + const std::string added = "BCHKLOTW"; + for (const char letter : added) { + const auto& glyph = AgcDriver::OverlayGlyph(letter); + require(std::any_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row != 0; }) && std::all_of(glyph.begin(), glyph.end(), [](std::uint8_t row) { return row <= 0x1f; }), std::string("the glyph of '") + letter + "' is empty or wider than 5 pixels"); + for (const char other : std::string("0123456789ADEFGIMNPRSUVX") + added) { + if (other != letter) require(glyph != AgcDriver::OverlayGlyph(other), std::string("'") + letter + "' and '" + other + "' have the same glyph"); + } + } +} + +void sampleTests() { + require(AgcDriver::OverlaySampleDue(std::nullopt, Start), "the first frame is not sampled"); + require(!AgcDriver::OverlaySampleDue(Start, Start + 999ms) && AgcDriver::OverlaySampleDue(Start, Start + 1000ms), "memory is sampled more than once a second, or not after a second"); + static std::uint8_t* volatile touched = nullptr; + const auto before = AgcDriver::ReadProcessPrivateBytes(); + require(before.has_value() && *before != 0, "the process's private memory cannot be read"); + constexpr std::size_t Bytes = std::size_t{64} << 20u; + const auto block = std::make_unique(Bytes); + touched = block.get(); + std::memset(touched, 0x5a, Bytes); + const auto after = AgcDriver::ReadProcessPrivateBytes(); + require(after.has_value() && *after >= *before + Bytes * 3 / 4, "touching 64 MiB raised the private memory from " + std::to_string(*before) + " to " + std::to_string(after.value_or(0)) + " bytes"); + require(touched[Bytes - 1] == 0x5a, "the touched block was not written"); +} + +void costReport() { + auto& counters = AgcDriver::PacketCounters::ThisThread(); + constexpr int Increments = 20'000'000; + const auto counted = Clock::now(); + for (int i = 0; i < Increments; ++i) counters.CountDraw(); + const auto countNs = std::chrono::duration(Clock::now() - counted).count() / Increments; + const auto summed = Clock::now(); + for (int i = 0; i < 1000; ++i) static_cast(AgcDriver::PacketCounters::Sum()); + const auto sumUs = std::chrono::duration(Clock::now() - summed).count() / 1000; + const auto sampled = Clock::now(); + for (int i = 0; i < 100; ++i) static_cast(AgcDriver::ReadProcessPrivateBytes()); + const auto ramUs = std::chrono::duration(Clock::now() - sampled).count() / 100; + std::printf("hidden cost: %.2f ns per counted draw or dispatch packet; shown: %.2f us to sum the packet counters per frame, %.1f us to read the private memory once a second\n", countNs, sumUs, ramUs); +} + void paletteTests() { const auto sdr = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); require(sdr[1] == std::array{1.0f, 1.0f, 1.0f, 1.0f} && sdr[0][3] > 0.0f && sdr[0][3] < 1.0f, "the SDR palette is not opaque white text over a translucent panel"); @@ -296,7 +479,14 @@ int main() { composeTests(); layoutTests(); paletteTests(); - std::cout << "performance overlay statistics, layout and palette tests passed\n"; + packetStatisticsTests(); + packetCounterTests(); + countFormatTests(); + detailComposeTests(); + fullPanelLayoutTests(); + sampleTests(); + costReport(); + std::cout << "performance overlay statistics, layout, palette, packet, pipeline, memory and resolution tests passed\n"; return 0; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 5594cda62f49cba863179d60faccc5bb739b0f6f Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:50:38 +0000 Subject: [PATCH 015/131] docs: describe the performance overlay's new lines USAGE.md explains DRAWS, SHADERS, VRAM, RAM and RES, where each number comes from, the count and memory formats, and what counting costs while the overlay is hidden. --- docs/user/USAGE.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 8c561c4459..a7dcafec49 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -196,8 +196,13 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | | `GPU` | GPU time per frame over the last second, or of the last frame below 1 fps, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second, or by a frame below 1 fps. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | | `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | +| `DRAWS` | Draw packets per frame over the last second, or of the last frame below 1 fps, then dispatch packets per frame (`DISPATCH`), as the packet workers execute them; packets that predication skips are not counted. Shown once the title has submitted either. | +| `SHADERS` | Pipelines created in the last second, then since the start (`TOTAL`): a graphics pipeline for each draw state the pipeline cache had not seen and a compute pipeline for each new dispatch shader. A rise while the game stutters points at pipeline compilation. Shown once a pipeline was created. | +| `VRAM` | Usage and budget of the GPU's largest device-local memory heap from `VK_EXT_memory_budget`, read once a second. Not shown when the driver lacks the extension. | +| `RAM` | The process's private memory, read once a second: `PrivateUsage` of `GetProcessMemoryInfo` on Windows, `RssAnon` plus `VmSwap` of `/proc/self/status` on Linux. The title's direct memory is shared memory on both systems and is not included. | +| `RES` | The display buffer's size, then the window's swapchain size, for example `RES: 1920X1080 / 2560X1440`. Not shown for frames without a display buffer. | -Times are shown with one decimal below 100 and as whole numbers above; anything above 9999 shows as 9999, so `FRAME` and `MIN/AVG/MAX` read 9999 for frames of 10 s or more. `FPS` has the same format, with two decimals below 1. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. +Times are shown with one decimal below 100 and as whole numbers above; anything above 9999 shows as 9999, so `FRAME` and `MIN/AVG/MAX` read 9999 for frames of 10 s or more. `FPS` has the same format, with two decimals below 1. Counts are whole numbers, in thousands (`12K`) from 10000 and in millions (`3M`) from a million. Memory is in GB of 2^30 bytes. While the overlay is hidden the presentation records nothing for it and the presented image is unchanged; while it is shown, one small render pass draws it over each presented frame. Each draw and dispatch packet and each pipeline creation is counted also while the overlay is hidden, with one increment of a counter that belongs to the packet worker or, for pipelines, a shared one; the memory and the counters are read only while the overlay is shown. ### Windows crash diagnostics From 8a1f298e6c46c9cd1ca5ab70e27e1bb6651e917f Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:38:55 -0700 Subject: [PATCH 016/131] fix(agc): count the last frame's pipelines in the performance overlay below 1 fps SHADERS counted the pipelines created between the first and the last present of the last second, so below 1 fps, with one present in that second, it read 0, as FPS did before. It now counts those created during the last frame there, like the other lines. The statistics test checks DRAWS, DISPATCH and SHADERS for a present alone in its second. --- .../prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp | 2 +- .../libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp | 4 +++- docs/user/USAGE.md | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index d1c24864a0..42abaffea0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -261,7 +261,7 @@ double OverlayStatistics::DispatchesPerFrame() const { std::uint64_t OverlayStatistics::PipelinesInWindow() const { if (window.empty()) return 0; - const auto first = window.front().counters.pipelines; + const auto first = window.size() < 2 && previous ? previous->counters.pipelines : window.front().counters.pipelines; const auto last = window.back().counters.pipelines; return last >= first ? last - first : 0; } diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index 583565c9f2..54d25e7412 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -312,9 +312,11 @@ void packetStatisticsTests() { require(near(statistics.GpuMsPerFrame(), 4.0) && near(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters"); auto stalled = statistics; auto counters = stalled.Latest(); + counters.draws += 900; + counters.dispatches += 40; counters.pipelines += 7; stalled.AddPresent(Start + 3000ms, counters); - require(stalled.DrawsPerFrame() == 0 && stalled.PipelinesInWindow() == 0 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as packets or pipelines of that second"); + require(near(stalled.DrawsPerFrame(), 900) && near(stalled.DispatchesPerFrame(), 40) && stalled.PipelinesInWindow() == 7 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as " + std::to_string(stalled.DrawsPerFrame()) + " draws, " + std::to_string(stalled.DispatchesPerFrame()) + " dispatches and " + std::to_string(stalled.PipelinesInWindow()) + " pipelines, not the last frame's 900, 40 and 7"); auto restarted = statistics; counters = restarted.Latest(); counters.draws = 3; diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index a7dcafec49..90b408bc5c 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -197,7 +197,7 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | `GPU` | GPU time per frame over the last second, or of the last frame below 1 fps, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second, or by a frame below 1 fps. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | | `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | | `DRAWS` | Draw packets per frame over the last second, or of the last frame below 1 fps, then dispatch packets per frame (`DISPATCH`), as the packet workers execute them; packets that predication skips are not counted. Shown once the title has submitted either. | -| `SHADERS` | Pipelines created in the last second, then since the start (`TOTAL`): a graphics pipeline for each draw state the pipeline cache had not seen and a compute pipeline for each new dispatch shader. A rise while the game stutters points at pipeline compilation. Shown once a pipeline was created. | +| `SHADERS` | Pipelines created in the last second, or during the last frame below 1 fps, then since the start (`TOTAL`): a graphics pipeline for each draw state the pipeline cache had not seen and a compute pipeline for each new dispatch shader. A rise while the game stutters points at pipeline compilation. Shown once a pipeline was created. | | `VRAM` | Usage and budget of the GPU's largest device-local memory heap from `VK_EXT_memory_budget`, read once a second. Not shown when the driver lacks the extension. | | `RAM` | The process's private memory, read once a second: `PrivateUsage` of `GetProcessMemoryInfo` on Windows, `RssAnon` plus `VmSwap` of `/proc/self/status` on Linux. The title's direct memory is shared memory on both systems and is not included. | | `RES` | The display buffer's size, then the window's swapchain size, for example `RES: 1920X1080 / 2560X1440`. Not shown for frames without a display buffer. | From 907fc73d1405a2872bc3164700d87500ff54d04c Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:44:30 -0700 Subject: [PATCH 017/131] feat(agc): mark the performance overlay PROFILED while a profile switch is on GPU and DRIVER need APS5_PROFILE_GPU or APS5_PROFILE_DRAW, and those switches add timestamp queries to every batch and timers to every packet, which changes the timings the panel shows. A screenshot of such a run could pass for an unprofiled one, so while either switch is on the panel now starts with a PROFILED line. --- .../libSceAgcDriver/Execution/src/PerformanceOverlay.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp | 2 +- .../libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp | 7 +++++-- docs/user/USAGE.md | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index 42abaffea0..d7b2b9aebf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -375,6 +375,7 @@ OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, boo OverlayContent ComposeOverlay(const OverlayStatistics& statistics, bool gpu, bool driver, const OverlayDetails& details) { OverlayContent content; + if (gpu || driver) content.above.push_back("PROFILED"); content.above.push_back("FPS: " + FormatOverlayRate(statistics.Fps())); content.above.push_back("FRAME: " + FormatOverlayValue(statistics.LastFrameMs()) + " MS"); content.frameTimes = statistics.FrameTimes(); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index 0992ffadeb..ceb94348e8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -320,7 +320,7 @@ OverlayContent fullPanel(const AgcDriver::OverlayDetails& details) { statistics.AddPresent(start + std::chrono::nanoseconds(16'666'667) * i, counters); } auto content = AgcDriver::ComposeOverlay(statistics, true, true, details); - Require(content.above.front() == "FPS: 60.0" && content.below.size() == 5u + (details.vram ? 1u : 0u) + (details.ram ? 1u : 0u) + (details.display.width != 0 ? 1u : 0u), "the full panel composes as '" + content.above.front() + "' with " + std::to_string(content.below.size()) + " lines below its graph"); + Require(content.above.size() == 3 && content.above[0] == "PROFILED" && content.above[1] == "FPS: 60.0" && content.below.size() == 5u + (details.vram ? 1u : 0u) + (details.ram ? 1u : 0u) + (details.display.width != 0 ? 1u : 0u), "the full panel composes as '" + content.above.front() + "' with " + std::to_string(content.below.size()) + " lines below its graph"); return content; } diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index 54d25e7412..aaf594a7db 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -111,7 +111,7 @@ void lowRateTests() { require(near(slow.GpuMsPerFrame(), static_cast(Gpu[i]) / 1e6) && near(slow.DriverMsPerFrame(), static_cast(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own"); } const auto everyTwoSeconds = AgcDriver::ComposeOverlay(slow, true, true); - require(everyTwoSeconds.above[0] == "FPS: 0.50" && everyTwoSeconds.above[1] == "FRAME: 2000 MS", "a present every 2 s shows '" + everyTwoSeconds.above[0] + "' and '" + everyTwoSeconds.above[1] + "'"); + require(everyTwoSeconds.above == std::vector{"PROFILED", "FPS: 0.50", "FRAME: 2000 MS"}, "a present every 2 s shows " + std::to_string(everyTwoSeconds.above.size()) + " lines above the graph, the last '" + everyTwoSeconds.above.back() + "'"); require(everyTwoSeconds.below[0] == "MIN/AVG/MAX: 2000/2000/2000" && everyTwoSeconds.below[1] == "GPU: 20.0 MS" && everyTwoSeconds.below[2] == "DRIVER: 8.0 MS", "a present every 2 s shows '" + everyTwoSeconds.below[0] + "', '" + everyTwoSeconds.below[1] + "' and '" + everyTwoSeconds.below[2] + "'"); OverlayStatistics glacial; @@ -119,7 +119,7 @@ void lowRateTests() { for (int i = 1; i <= 5; ++i) glacial.AddPresent(Start + i * 12s, static_cast(i) * 250'000'000u, static_cast(i) * 12'500'000'000u); require(near(glacial.Fps(), 1000.0 / 12000.0) && near(glacial.LastFrameMs(), 12000) && near(glacial.GpuMsPerFrame(), 250) && near(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms"); const auto everyTwelveSeconds = AgcDriver::ComposeOverlay(glacial, true, true); - require(everyTwelveSeconds.above[0] == "FPS: 0.08" && everyTwelveSeconds.above[1] == "FRAME: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.above[0] + "' and '" + everyTwelveSeconds.above[1] + "'"); + require(everyTwelveSeconds.above == std::vector{"PROFILED", "FPS: 0.08", "FRAME: 9999 MS"}, "a present every 12 s shows " + std::to_string(everyTwelveSeconds.above.size()) + " lines above the graph, the last '" + everyTwelveSeconds.above.back() + "'"); require(everyTwelveSeconds.below[0] == "MIN/AVG/MAX: 9999/9999/9999" && everyTwelveSeconds.below[1] == "GPU: 250 MS" && everyTwelveSeconds.below[2] == "DRIVER: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.below[0] + "', '" + everyTwelveSeconds.below[1] + "' and '" + everyTwelveSeconds.below[2] + "'"); require(everyTwelveSeconds.frameTimes == std::vector(5, 12000.0), "the graph does not hold the five 12 s frames"); const auto layout = AgcDriver::LayOutOverlay(everyTwelveSeconds, {1920, 1080}, OverlayCorner::TopLeft); @@ -187,6 +187,9 @@ void composeTests() { require(plain.frameTimes.size() == 100, "the graph does not hold every frame time"); const auto profiled = AgcDriver::ComposeOverlay(statistics, true, true); require(profiled.below.size() == 3 && profiled.below[1] == "GPU: 2.0 MS" && profiled.below[2] == "DRIVER: 0.5 MS", "the profiled lines read '" + profiled.below[1] + "' and '" + profiled.below[2] + "'"); + std::vector marked{"PROFILED"}; + marked.insert(marked.end(), plain.above.begin(), plain.above.end()); + require(profiled.above == marked && AgcDriver::ComposeOverlay(statistics, true, false).above == marked && AgcDriver::ComposeOverlay(statistics, false, true).above == marked, "a profiled panel does not start with PROFILED above the unprofiled lines: it starts with '" + profiled.above.front() + "'"); require(AgcDriver::ComposeOverlay(statistics, true, false).below.size() == 2 && AgcDriver::ComposeOverlay(statistics, false, true).below[1].rfind("DRIVER", 0) == 0, "a line is shown without its source"); } diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 90b408bc5c..29f34b4fd0 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -190,6 +190,7 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | Line | Meaning | |------|---------| +| `PROFILED` | The first line while `APS5_PROFILE_GPU=1` or `APS5_PROFILE_DRAW=1` is set. Those switches add timestamp queries to every batch and timers to every packet, so the numbers of such a run aren't comparable with an unprofiled one. | | `FPS` | Frames presented per second, over the last second. When the last second holds fewer than two presents (below 1 fps), the rate of the last frame instead, 1000 divided by its time in ms: a frame every 2 s shows `0.50`. Every present counts, including frames a title presents cleared, for example while it loads. | | `FRAME` | Time between the last two presented frames, in ms. | | graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | From 46abaab054693ed6714ef3bc39ab4925f4ecb9ea Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 13:16:26 -0700 Subject: [PATCH 018/131] refactor(agc): count compute pipelines in the helper that creates them The overlay's SHADERS count for compute pipelines sat in VulkanDevice::dispatch, inside a block that another open change moves. CreateComputePipeline now counts each pipeline it creates, so the count follows the creation wherever it runs. The capture replay path, which creates the same title pipelines, is counted too. --- core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 59d5ce689e..a77efedf17 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -114,6 +114,7 @@ void CreateComputePipeline(VkDevice device, PFN_vkCreateComputePipelines create, info.layout = layout; auto cacheLock = synchronization != nullptr ? synchronization->LockForPipelineUse() : Graphics::PipelineCacheSynchronization::PipelineUseLock{}; check(create(device, cache, 1, &info, nullptr, pipeline), "vkCreateComputePipelines"); + PerformanceCounters::CountPipeline(); } bool CaptureComputePipeline(std::size_t words) { @@ -4540,7 +4541,6 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec CreateComputePipeline(state->device, state->DeviceFunction("vkCreateComputePipelines"), context.pipelineCache, objects->module, objects->layout, pipelineFlags, 0, specialization.Info(), subgroupSize, &objects->pipeline, context.pipelineCacheSynchronization); objects->destroyModule(objects->device, objects->module, nullptr); objects->module = VK_NULL_HANDLE; - PerformanceCounters::CountPipeline(); Graphics::LogPipelineStatistics_nid_no_patch(context, objects->pipeline); timing.Mark("pipeline_create"); if (pipelineKey != 0) { From 4ceb020b551947064c9237059bc37cc279f8e753 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 00:20:43 +0400 Subject: [PATCH 019/131] fix(relinker): trap guest GS accesses in Windows output instead of refusing the module On Windows the host owns GS (the thread environment block), so a guest instruction that reads or writes through GS cannot be translated the way FS thread-pointer accesses are. Such an instruction refused the whole module. Overwrite it in place with ud2 and NOP padding instead, so the module converts and the instruction reports itself through the crash reporter if it runs. --- .../src/windows/WindowsTlsBuilder.cpp | 22 +++++++++++++++++++ .../tests/test_tls_function_coverage.py | 1 - 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index 2a017013c4..faac015edb 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -90,6 +91,20 @@ void patchAccess(std::vector& sections, const TlsAccess& access, cons throw Domain::RelinkerException("TLS instruction is outside the PE image", access.Rva); } +void trapInstruction(std::vector& sections, const std::uint32_t rva, const std::size_t length) { + for (auto& section : sections) { + if (rva < section.Rva || rva - section.Rva >= section.Data.size()) + continue; + const auto offset = rva - section.Rva; + if (length > section.Data.size() - offset) + throw Domain::RelinkerException("Guest GS instruction crosses a PE section boundary", rva); + std::fill_n(section.Data.begin() + offset, length, 0x90); + std::copy(std::begin(kStubOpUd2), std::end(kStubOpUd2), section.Data.begin() + offset); + return; + } + throw Domain::RelinkerException("Guest GS instruction is outside the PE image", rva); +} + } PeDirectory WindowsTlsBuilder::Build(const std::vector& source, const std::vector& headers, const WindowsLoadImage& image, std::vector& sections, std::vector& relocations, std::uint32_t& nextRva, std::uint32_t* tlsIndexRva, const std::function& progress) const { @@ -98,6 +113,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co const Codegen::X64InstructionDecoder decoder; std::vector accesses; + std::vector> gsTraps; std::set branchTargets; const auto instructions = Relinker::CodeInstructionCollector().Collect(source, headers, [&](auto completed, auto total) { report(7000 * completed / total); }); std::uint64_t scanned = 0; @@ -127,6 +143,10 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co branchTargets.insert(static_cast(target)); } + if (info.SegmentPrefix == 0x65 && !insidePrevious) { + gsTraps.emplace_back(rva, info.Length); + continue; + } if (info.SegmentPrefix != 0 && !(insidePrevious && info.SegmentPrefix != 0x64)) { const auto position = info.OpcodeOffset; bool hasOperandSizePrefix = false; @@ -192,6 +212,8 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co } report(9000); + for (const auto& [rva, length] : gsTraps) + trapInstruction(sections, rva, length); if (tls == nullptr) { if (!accesses.empty()) throw Domain::RelinkerException("Guest TLS access without PT_TLS"); diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 8061ee1638..371a3204e7 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -735,7 +735,6 @@ def main(): "rsp-displacement": fs_load(4, 40), "rsp-alu": fs_alu(0x33, 4, 40), "dword-load": bytes.fromhex("64 8b 04 25 28 00 00 00"), - "gs-load": bytes.fromhex("65 48 8b 04 25 28 00 00 00"), "rex-b-load": bytes.fromhex("64 49 8b 04 25 28 00 00 00"), "rsp-base-load": bytes.fromhex("64 48 8b 04 24"), "rsp-register-address": bytes.fromhex("64 48 8b 20"), From 7b9b91765d0a051c9c9ecc45df0262eed2e99d5b Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 00:21:48 +0400 Subject: [PATCH 020/131] test(relinker): cover GS loads, stores and ALU reads trapped in Windows output Each form converts and is replaced by ud2 and NOP padding of its own length, leaving the next instruction in place. The forms are the ones Wine's ntdll in Prospero Win (PPSA99995) uses, plus a register-address form shorter than the jump an FS access is rewritten with. --- core/relinker/CMakeLists.txt | 1 + .../tests/test_tls_function_coverage.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 4660de2490..9839446231 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -414,6 +414,7 @@ if(BUILD_TESTING) foreach(opcode IN ITEMS add or adc sbb and sub xor cmp) list(APPEND tlsCoverageGroups alu_${opcode}) endforeach() + list(APPEND tlsCoverageGroups gs_traps) foreach(group IN LISTS tlsCoverageGroups) add_test(NAME tls_function_coverage_${group} COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_tls_function_coverage.py $ ${group}) diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index 371a3204e7..b36211185c 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -511,7 +511,7 @@ def add_alias(image, size, begin=0x1200): def main(): groups = ("metadata", "padding", "register_loads", "displacement_bounds", "alu_execution", - "register_address_loads", "moved_branch", "rejected") + "register_address_loads", "moved_branch", "rejected", "gs_traps") groups += tuple(f"displacement_r{register}" for register in range(16) if register != 4) groups += tuple(f"alu_{name}" for name in ALU_READ) parser = argparse.ArgumentParser(description="Run Windows guest TLS function coverage fixtures") @@ -751,6 +751,22 @@ def main(): conflicting[232:344] = unwind[232:344] struct.pack_into(" Date: Thu, 15 Oct 2026 00:22:32 +0400 Subject: [PATCH 021/131] test(relinker): trap a GS access in a module without a TLS segment The Windows TLS builder returns early for a module without PT_TLS. A GS access there must still be trapped; left in place it would read the host's thread environment block. --- .../relinker/tests/test_tls_function_coverage.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/core/relinker/relinker/tests/test_tls_function_coverage.py b/core/relinker/relinker/tests/test_tls_function_coverage.py index b36211185c..e0897cb6dc 100644 --- a/core/relinker/relinker/tests/test_tls_function_coverage.py +++ b/core/relinker/relinker/tests/test_tls_function_coverage.py @@ -767,6 +767,17 @@ def main(): assert result.returncode == 0, (name, result.stdout, result.stderr) patched = pe_bytes_at(output.read_bytes(), 0x11240, len(instruction) + 1) assert patched == b"\x0f\x0b" + b"\x90" * (len(instruction) - 2) + b"\xc3", (name, patched.hex()) + instruction = bytes.fromhex("65 48 8b 0c 25 78 03 00 00") + image = make_image("register", "unwind", body=instruction + b"\xc3") + struct.pack_into(" Date: Wed, 14 Oct 2026 23:34:42 +0200 Subject: [PATCH 022/131] fix(libkernel): preserve condition signals across cancellation --- .../prx/libkernel/Pthread/include/Pthread.hpp | 3 +- core/libs/prx/libkernel/Pthread/src/Cond.cpp | 59 +++--- .../libs/tests/GuestPthreadCancelRecovery.cpp | 186 ++++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 4 files changed, 223 insertions(+), 27 deletions(-) diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index e22426757f..6b1c2c0ff1 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -61,7 +62,7 @@ struct PthreadCondattrPrivate { }; struct PthreadCondPrivate { - TimedWait::Condition _cv; + std::shared_ptr _cv = std::make_shared(); int _clockid = 0; }; diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index d576c64ff0..c177d2a5ad 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -2,6 +2,7 @@ #include "prx/libkernel/Pthread/include/Mutex.hpp" #include "prx/libkernel/Pthread/include/Cond.hpp" #include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" #include "prx/libkernel/Pthread/Posix/Common.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/Time.hpp" @@ -13,6 +14,7 @@ #include #include #include +#include namespace { @@ -31,21 +33,20 @@ PthreadMutex adaptiveInitializer() { return reinterpret_cast(std::uintptr_t{1}); } -PthreadCond resolveCond(PthreadCond* cond) { +std::shared_ptr resolveCond(PthreadCond* cond, int* clockId = nullptr) { if (!cond) throw std::invalid_argument("Condition variable pointer is null"); - std::atomic_ref slot(*cond); - if (const auto current = slot.load(std::memory_order_acquire); current && current != destroyedCond()) - return current; std::lock_guard lock(condInitializationMutex); - const auto current = slot.load(std::memory_order_acquire); + std::atomic_ref slot(*cond); + auto current = slot.load(std::memory_order_acquire); if (current == destroyedCond()) throw std::runtime_error("Condition variable has been destroyed"); - if (current) - return current; - auto* created = new PthreadCondPrivate(); - slot.store(created, std::memory_order_release); - return created; + if (!current) { + current = new PthreadCondPrivate(); + slot.store(current, std::memory_order_release); + } + if (clockId) *clockId = current->_clockid; + return current->_cv; } int mutexOwnership(PthreadMutex* mutex, PthreadMutex* owned) { @@ -73,8 +74,16 @@ struct WakingLock { } }; -int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { - auto* c = resolveCond(cond); +int finishWait(std::shared_ptr& condition, bool timedOut) { + if (ThreadCancel::Requested()) { + condition->NotifyOne(); + condition.reset(); + scePthreadExit(GUEST_CANCELED); + } + return timedOut ? sceTimedOut : 0; +} + +int waitUntil(std::shared_ptr condition, PthreadMutex* mutex, std::optional deadlineNanos, const void* caller) { PthreadMutex m = nullptr; if (const int error = mutexOwnership(mutex, &m)) return error; @@ -92,32 +101,32 @@ int waitUntil(PthreadCond* cond, PthreadMutex* mutex, std::optional_owner.store(std::thread::id{}, std::memory_order_release); for (int level = 1; level < previousCount; ++level) m->_rmtx.unlock(); - timedOut = !ThreadCancel::Wait(c->_cv, lock, deadlineNanos); + timedOut = !ThreadCancel::Wait(*condition, lock, deadlineNanos); for (int level = 1; level < previousCount; ++level) m->_rmtx.lock(); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); m->_count = previousCount; lock.release(); - ThreadCancel::Check(); - return timedOut ? sceTimedOut : 0; + return finishWait(condition, timedOut); } WakingLock waking{m, m->_mtx}; std::unique_lock lock(waking, std::adopt_lock); m->_owner.store(std::thread::id{}, std::memory_order_release); - timedOut = !ThreadCancel::Wait(c->_cv, lock, deadlineNanos); + timedOut = !ThreadCancel::Wait(*condition, lock, deadlineNanos); m->_owner.store(std::this_thread::get_id(), std::memory_order_release); lock.release(); - ThreadCancel::Check(); - return timedOut ? sceTimedOut : 0; + return finishWait(condition, timedOut); } } int CondOperations::AbsoluteTimedwait(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime) { KernelUseconds usec = 0; - if (!PosixThread::RelativeMicroseconds(resolveCond(cond)->_clockid, abstime, &usec)) + int clockId = 0; + auto condition = resolveCond(cond, &clockId); + if (!PosixThread::RelativeMicroseconds(clockId, abstime, &usec)) throw std::invalid_argument("Invalid absolute condition variable timeout"); - return waitUntil(cond, mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); + return waitUntil(std::move(condition), mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); } extern "C" { @@ -169,27 +178,27 @@ int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) { } int APS5_VABI scePthreadCondSignal(PthreadCond* cond) { - resolveCond(cond)->_cv.NotifyOne(); + resolveCond(cond)->NotifyOne(); return 0; } int APS5_VABI scePthreadCondBroadcast(PthreadCond* cond) { - resolveCond(cond)->_cv.NotifyAll(); + resolveCond(cond)->NotifyAll(); return 0; } int APS5_VABI scePthreadCondSignalto(PthreadCond* cond, Pthread thread) { (void)thread; - resolveCond(cond)->_cv.NotifyAll(); + resolveCond(cond)->NotifyAll(); return 0; } int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex) { - return waitUntil(cond, mutex, std::nullopt, __builtin_return_address(0)); + return waitUntil(resolveCond(cond), mutex, std::nullopt, __builtin_return_address(0)); } int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec) { - return waitUntil(cond, mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); + return waitUntil(resolveCond(cond), mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); } } diff --git a/core/libs/tests/GuestPthreadCancelRecovery.cpp b/core/libs/tests/GuestPthreadCancelRecovery.cpp index 59e6056c85..297f761a92 100644 --- a/core/libs/tests/GuestPthreadCancelRecovery.cpp +++ b/core/libs/tests/GuestPthreadCancelRecovery.cpp @@ -1,10 +1,21 @@ #include "SceTypes.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #include #include +#include #include #include #include #include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -17,6 +28,9 @@ int APS5_VABI scePthreadKeyCreate(PthreadKey* key, pthread_key_destructor_func_t int APS5_VABI scePthreadKeyDelete(PthreadKey key); int APS5_VABI scePthreadSetspecific(PthreadKey key, void* value); int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); +int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); @@ -24,6 +38,10 @@ int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); int APS5_VABI scePthreadCondSignal(PthreadCond* cond); int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, KernelUseconds usec); +int APS5_VABI scePthreadCondWait(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI pthread_cond_wait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex); +int APS5_VABI pthread_cond_timedwait_nid_postfix(PthreadCond* cond, PthreadMutex* mutex, const KernelTimespec* abstime); +int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* time); int APS5_VABI scePthreadSemInit(PthreadSem* sem, int flag, unsigned int value, const char* name); int APS5_VABI scePthreadSemDestroy(PthreadSem* sem); int APS5_VABI scePthreadSemWait(PthreadSem* sem); @@ -226,6 +244,166 @@ static void ConditionRecovery() { Require(scePthreadMutexDestroy(&mutex) == 0); } +enum class ConditionWaitMode { SceTimed, Sce, PosixTimed, Posix }; + +struct SignaledCondition { + PthreadMutex mutex = nullptr; + PthreadCond condition = nullptr; + PthreadCond* slot = &condition; + ConditionWaitMode mode = ConditionWaitMode::SceTimed; + int lockCount = 1; + bool cancellationDisabled = false; + bool released = false; + std::atomic ready{0}; + std::array results{-1, -1}; + std::array cleaned{}; +}; + +struct SignaledConditionWorker { + SignaledCondition* context; + int index; +}; + +static void APS5_VABI UnlockSignaledCondition(void* value) { + auto& worker = *static_cast(value); + auto& context = *worker.context; + for (int level = 0; level < context.lockCount; ++level) Require(scePthreadMutexUnlock(&context.mutex) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == static_cast(0x80020001)); + context.cleaned[worker.index] = true; +} + +static void* APS5_VABI SignaledConditionWaiter(void* value) { + auto& worker = *static_cast(value); + auto& context = *worker.context; + if (context.cancellationDisabled) Require(scePthreadSetcancelstate(CANCEL_DISABLE, nullptr) == 0); + for (int level = 0; level < context.lockCount; ++level) Require(scePthreadMutexLock(&context.mutex) == 0); + std::uintptr_t cleanup[8]; + __pthread_cleanup_push_imp_nid_postfix(UnlockSignaledCondition, &worker, cleanup); + context.ready.fetch_add(1); + int result = 0; + do { + switch (context.mode) { + case ConditionWaitMode::SceTimed: + result = scePthreadCondTimedwait(context.slot, &context.mutex, 2000000); + break; + case ConditionWaitMode::Sce: + result = scePthreadCondWait(context.slot, &context.mutex); + break; + case ConditionWaitMode::PosixTimed: { + KernelTimespec deadline{}; + Require(clock_gettime_nid_postfix(0, &deadline) == 0); + deadline.tv_sec += 2; + result = pthread_cond_timedwait_nid_postfix(context.slot, &context.mutex, &deadline); + break; + } + case ConditionWaitMode::Posix: + result = pthread_cond_wait_nid_postfix(context.slot, &context.mutex); + break; + } + } while (result == 0 && !context.released); + context.results[worker.index] = result; + __pthread_cleanup_pop_imp_nid_postfix(1); + return nullptr; +} + +static bool ConditionRegistered(Pthread thread) { + const std::lock_guard lock(thread->cancelLock); + return thread->cancelWait != nullptr; +} + +static void InitializeSignaledCondition(SignaledCondition& context, bool recursive) { + PthreadMutexattr attr = nullptr; + Require(scePthreadMutexattrInit(&attr) == 0); + Require(scePthreadMutexattrSettype(&attr, recursive ? 2 : 1) == 0); + Require(scePthreadMutexInit(&context.mutex, &attr, nullptr) == 0); + Require(scePthreadMutexattrDestroy(&attr) == 0); + context.lockCount = recursive ? 3 : 1; + Require(scePthreadCondInit(context.slot, nullptr, nullptr) == 0); +} + +static void CanceledConditionSignal(ConditionWaitMode mode, bool recursive) { + SignaledCondition context; + context.mode = mode; + InitializeSignaledCondition(context, recursive); + std::array workers{{{&context, 0}, {&context, 1}}}; + std::array threads{}; + for (int index = 0; index < 2; ++index) { + Require(scePthreadCreate(&threads[index], nullptr, SignaledConditionWaiter, &workers[index], nullptr) == 0); + while (context.ready.load() <= index) std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (!ConditionRegistered(threads[index]) && std::chrono::steady_clock::now() < deadline) { + Require(scePthreadMutexUnlock(&context.mutex) == 0); + std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + } + Require(ConditionRegistered(threads[index])); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + } + Require(scePthreadMutexLock(&context.mutex) == 0); + context.released = true; + Require(scePthreadCondSignal(&context.condition) == 0); + int selected = -1; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (selected < 0 && std::chrono::steady_clock::now() < deadline) { + for (int index = 0; index < 2; ++index) if (!ConditionRegistered(threads[index])) selected = index; + std::this_thread::yield(); + } + Require(selected >= 0); + Require(scePthreadCancel(threads[selected]) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + Join(threads[selected], CANCELED); + Join(threads[1 - selected], nullptr); + Require(context.results[selected] == -1 && context.results[1 - selected] == 0); + Require(context.cleaned[0] && context.cleaned[1]); + Require(scePthreadCondDestroy(&context.condition) == 0); + Require(scePthreadMutexDestroy(&context.mutex) == 0); +} + +static void DestroyedSignaledCondition(ConditionWaitMode mode, bool recursive, bool cancel, bool cancellationDisabled) { + SignaledCondition context; + context.mode = mode; + context.cancellationDisabled = cancellationDisabled; +#ifdef _WIN32 + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const auto bytes = static_cast(system.dwPageSize); + context.slot = static_cast(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(context.slot != nullptr); +#else + const auto bytes = static_cast(sysconf(_SC_PAGESIZE)); + context.slot = static_cast(mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(context.slot != MAP_FAILED); +#endif + *context.slot = nullptr; + InitializeSignaledCondition(context, recursive); + const std::weak_ptr retained = (*context.slot)->_cv; + SignaledConditionWorker worker{&context, 0}; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, SignaledConditionWaiter, &worker, nullptr) == 0); + while (context.ready.load() == 0) std::this_thread::yield(); + Require(scePthreadMutexLock(&context.mutex) == 0); + context.released = true; + Require(scePthreadCondSignal(context.slot) == 0); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (ConditionRegistered(thread) && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + Require(!ConditionRegistered(thread)); + Require(scePthreadCondDestroy(context.slot) == 0); + Require(!retained.expired()); +#ifdef _WIN32 + Require(VirtualFree(context.slot, 0, MEM_RELEASE) != 0); +#else + Require(munmap(context.slot, bytes) == 0); +#endif + if (cancel) Require(scePthreadCancel(thread) == 0); + Require(scePthreadMutexUnlock(&context.mutex) == 0); + Join(thread, cancel && !cancellationDisabled ? CANCELED : nullptr); + Require(context.cleaned[0]); + Require(context.results[0] == (cancel && !cancellationDisabled ? -1 : 0)); + Require(retained.expired()); + Require(scePthreadMutexDestroy(&context.mutex) == 0); +} + static void* APS5_VABI HeldTarget(void*) { CheckDefaults(); targetRelease.wait(false); @@ -274,6 +452,14 @@ int main() { } Require(scePthreadKeyDelete(key) == 0); ConditionRecovery(); + for (const auto mode : {ConditionWaitMode::SceTimed, ConditionWaitMode::Sce, ConditionWaitMode::PosixTimed, ConditionWaitMode::Posix}) { + for (bool recursive : {false, true}) { + CanceledConditionSignal(mode, recursive); + DestroyedSignaledCondition(mode, recursive, true, false); + DestroyedSignaledCondition(mode, recursive, false, false); + DestroyedSignaledCondition(mode, recursive, true, true); + } + } disabled = true; ConditionRecovery(); JoinRecovery(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index efa9156809..a2afebc98f 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -51,7 +51,7 @@ - [sceKernelSyncOnAddressWait](../../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [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 +- [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 - [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 - [sceKernelGetAvailableCpumask](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed From e90616b16c592f25d417b67106a8f485278999cf Mon Sep 17 00:00:00 2001 From: notaxeltv Date: Wed, 14 Oct 2026 23:35:41 +0200 Subject: [PATCH 023/131] feat(libc): ignore name in _Cnd_init_with_default_name_override Absent PPSA34015 import; forward to _Cnd_init like Ampr's named init. Co-authored-by: Cursor --- core/libs/prx/libc/CMakeLists.txt | 12 ++++++ core/libs/prx/libc/src/Threads.cpp | 4 ++ .../tests/GuestLibcCndInitDefaultName.cpp | 42 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 core/libs/tests/GuestLibcCndInitDefaultName.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 02162709a5..7817307f16 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -692,6 +692,18 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_libc_cnd_init_default_name_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndInitDefaultName.cpp) +target_include_directories(guest_libc_cnd_init_default_name_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_libc_cnd_init_default_name_tests PRIVATE libc) +configure_windows_unwind(guest_libc_cnd_init_default_name_tests) +if(BUILD_TESTING) + set_target_properties(guest_libc_cnd_init_default_name_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_libc_cnd_init_default_name COMMAND guest_libc_cnd_init_default_name_tests) + if(WIN32) + set_tests_properties(guest_libc_cnd_init_default_name PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(guest_libc_cnd_timed_wait_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndTimedWait.cpp) target_include_directories(guest_libc_cnd_timed_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_libc_cnd_timed_wait_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index b1d85f9faf..28abb1454b 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -187,6 +187,10 @@ int APS5_VABI _Cnd_init_nid_postfix(void** handle) { return threadSuccess; } +int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char*) { + return _Cnd_init_nid_postfix(handle); +} + void APS5_VABI _Cnd_destroy_nid_postfix(void** handle) { if (!handle) throw std::invalid_argument("_Cnd_destroy: null condition handle pointer"); diff --git a/core/libs/tests/GuestLibcCndInitDefaultName.cpp b/core/libs/tests/GuestLibcCndInitDefaultName.cpp new file mode 100644 index 0000000000..6d5d046398 --- /dev/null +++ b/core/libs/tests/GuestLibcCndInitDefaultName.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI _Cnd_init_with_default_name_override_nid_postfix(void** handle, const char* name); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_signal_nid_postfix(void** handle); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int success = 0; + +int main() { + void* condition = nullptr; + Require(_Cnd_init_with_default_name_override_nid_postfix(&condition, "GameCond") == success); + Require(condition != nullptr); + Require(_Cnd_signal_nid_postfix(&condition) == success); + _Cnd_destroy_nid_postfix(&condition); + Require(condition == nullptr); + + void* unnamed = nullptr; + Require(_Cnd_init_with_default_name_override_nid_postfix(&unnamed, nullptr) == success); + Require(unnamed != nullptr); + _Cnd_destroy_nid_postfix(&unnamed); + + RequireThrows([] { + _Cnd_init_with_default_name_override_nid_postfix(nullptr, "NullHandle"); + }); + return 0; +} From a7730cb9e600b67a303910cc29bf8ae8e3e23cce Mon Sep 17 00:00:00 2001 From: AnyPS5 Contributors Date: Wed, 14 Oct 2026 17:48:14 -0400 Subject: [PATCH 024/131] fix(graphics): keep discarded import probes from pinning windows --- .../Graphics/src/GuestBufferMemory.cpp | 23 ++- .../libSceAgcDriver/tests/BdaResources.cpp | 147 ++++++++++++++++++ 2 files changed, 164 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index c04d2bdf93..1674531dd1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -892,6 +892,7 @@ ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin return run; } +const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end); const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end); bool importsStale(const Context& context, const HostImports& state); const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict); @@ -910,7 +911,10 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s if (fresh && end != 0) { if (const auto* current = findImport(state, address, end)) return current; } else if (!fresh) { - if (const auto* current = findImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) return current; + if (const auto* current = peekImport(state, address, address + 1); current != nullptr && current->joined && sameRange(*current, lease)) { + current->lastUse = ++state.useCounter; + return current; + } } auto run = importRun(lease, address); // Without an end the caller views the whole run through the import (AcquireRegistered): a window that holds the @@ -941,7 +945,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import failures.insert_or_assign(base, ImportFailure{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end())}); return nullptr; } - if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) return &found->second; + if (const auto found = state.imports.find(base); found != state.imports.end() && sameRun(found->second, run)) { + found->second.lastUse = ++state.useCounter; + return &found->second; + } if (alignment == 0 || base % alignment != 0 || bytes % alignment != 0) return nullptr; if (const auto failed = failures.find(base); failed != failures.end()) { if (sameRun(failed->second, run)) return nullptr; @@ -1153,14 +1160,18 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it); } -const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { +const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end) { auto found = state.imports.upper_bound(begin); if (found == state.imports.begin()) return nullptr; --found; const auto& entry = found->second; - if (begin < entry.base || end > entry.base + entry.bytes) return nullptr; - entry.lastUse = ++state.useCounter; - return &entry; + return begin < entry.base || end > entry.base + entry.bytes ? nullptr : &entry; +} + +const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) { + const auto* entry = peekImport(state, begin, end); + if (entry != nullptr) entry->lastUse = ++state.useCounter; + return entry; } const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) { diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 2fe20f100c..b79cab3909 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -608,6 +609,151 @@ void importWholeRangeOverWindowTests(const Context& context) { leased.WriteBack(); } +struct ObservedHostImport { + const void* pointer; + VkDeviceSize bytes; +}; + +class ImportAllocationProbe { +public: + explicit ImportAllocationProbe(Context& context) : context(context), original(context.deviceProc) { + Require(active == nullptr && original != nullptr, "invalid import allocation probe scope"); + active = this; + context.deviceProc = Resolve; + } + ~ImportAllocationProbe() { + context.deviceProc = original; + active = nullptr; + } + ImportAllocationProbe(const ImportAllocationProbe&) = delete; + ImportAllocationProbe& operator=(const ImportAllocationProbe&) = delete; + + std::vector calls; + +private: + static PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice device, const char* name) { + Require(active != nullptr, "import allocation probe escaped its scope"); + if (std::strcmp(name, "vkAllocateMemory") == 0) return reinterpret_cast(Allocate); + return active->original(device, name); + } + static VkResult VKAPI_CALL Allocate(VkDevice device, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks* allocator, VkDeviceMemory* memory) { + Require(active != nullptr, "import allocation callback escaped its scope"); + for (const auto* next = static_cast(info->pNext); next != nullptr; next = next->pNext) { + if (next->sType != VK_STRUCTURE_TYPE_IMPORT_MEMORY_HOST_POINTER_INFO_EXT) continue; + const auto* imported = reinterpret_cast(next); + active->calls.push_back({imported->pHostPointer, info->allocationSize}); + } + const auto allocate = reinterpret_cast(active->original(device, "vkAllocateMemory")); + Require(allocate != nullptr, "import allocation probe has no original allocator"); + return allocate(device, info, allocator, memory); + } + + Context& context; + PFN_vkGetDeviceProcAddr original; + inline static thread_local ImportAllocationProbe* active = nullptr; +}; + +class UnrelatedImportProbeRange { +public: + UnrelatedImportProbeRange() { + GuestAllocations::Mutation().Add(storage.data(), storage.size(), true, false, false); + } + ~UnrelatedImportProbeRange() { + GuestAllocations::Mutation().Remove(storage.data()); + } + UnrelatedImportProbeRange(const UnrelatedImportProbeRange&) = delete; + UnrelatedImportProbeRange& operator=(const UnrelatedImportProbeRange&) = delete; + +private: + std::array storage{}; +}; + +void importStaleProbeTests(const Context& context, const BdaTestAccess& access) { + for (const auto scenario : {0u, 1u, 2u}) { + const bool held = scenario == 1u; + const bool exactWhole = scenario == 2u; + WindowedRange range(context, 2 * WindowedRange::piece, 2); + ImportAllocationProbe probe(range.importing); + const auto first = range.First(); + auto* guest = reinterpret_cast(first); + for (std::size_t offset = 17u; offset < range.bytes; offset += 4096u) guest[offset] = static_cast((offset / 4096u) & 0xffu); + const std::vector originalGuest(guest, guest + range.bytes); + const auto* window = HostImportFor(range.importing, first, exactWhole ? range.bytes : 32u); + Require(window != nullptr && !window->joined && window->base == first && window->bytes == (exactWhole ? range.bytes : range.window), "the stale-probe fixture did not create its ordinary import extent"); + const auto originalBuffer = window->buffer; + const auto originalUse = window->lastUse; + const auto originalSerial = HostImportSerial(range.importing, first, 32u, false); + auto mockContents = access.bytes(window->buffer); + Require(mockContents.size() == window->bytes, "the mock import buffer has an incorrect extent"); + std::copy_n(originalGuest.begin(), window->bytes, mockContents.begin()); + std::uint32_t scratch = 0x12345678u; + { + GuestBufferMemory copied(range.importing); + copied.AddReadable(reinterpret_cast(&scratch), sizeof(scratch)); + copied.Upload(false); + copied.WriteBack(); + } + Require(window->lastUse == originalUse && HostImportSerial(range.importing, first, 32u, false) == originalSerial, "an unrelated copied build touched or replaced the prefix window"); + GuestBufferMemory reader(range.importing); + VkDescriptorBufferInfo heldView{}; + std::uint32_t heldAdjustment = 0u; + if (held) { + reader.AddReadable(first + 16u, 32u); + reader.Upload(false); + heldView = reader.Descriptor(first + 16u, 32u, heldAdjustment); + Require(heldView.buffer == originalBuffer, "the held control did not bind the prefix window"); + } + GuestAllocations::Mapped mapped; + { + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); + for (const auto& owner : lease) if (owner->address == first && owner->bytes == range.bytes) mapped.push_back(owner); + } + Require(mapped.size() == 1u, "the stale-probe fixture lost its registered range"); + const auto originalOwner = mapped.front().lock(); + const auto generation = GuestAllocations::GuestAllocationsGeneration_nid_postfix(); + UnrelatedImportProbeRange unrelated; + Require(GuestAllocations::GuestAllocationsGeneration_nid_postfix() != generation && HostImportsIdentity(range.importing).device == VK_NULL_HANDLE, + "the unrelated registry change did not make imports stale"); + const auto attempts = access.allocationAttempts(); + probe.calls.clear(); + { + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); + Require(ImportMappedRanges(range.importing, mapped, GuestAllocations::GuestAllocationsGeneration_nid_postfix(), false), "the current device did not admit the map-time import pass"); + } + Require(mapped.front().lock() == originalOwner, "an unrelated registry change replaced the import's memory owner"); + if (held) { + Require(access.allocationAttempts() == attempts && probe.calls.empty(), "a stale probe retired an import held by an actual descriptor"); + const auto* kept = HostImportFor(range.importing, first + 16u, 32u); + Require(kept != nullptr && kept->buffer == originalBuffer && kept->bytes == range.window && HostImportSerial(range.importing, first, 32u, false) == originalSerial, + "the actual held import lost its buffer, extent or serial"); + std::uint32_t adjustment = 0u; + const auto view = reader.Descriptor(first + 16u, 32u, adjustment); + Require(view.buffer == heldView.buffer && view.offset == heldView.offset && view.range == heldView.range && adjustment == heldAdjustment, + "a map-time stale probe invalidated a descriptor that still uses its import"); + const auto contents = access.bytes(view.buffer); + Require(view.offset + adjustment + 32u <= contents.size() && std::memcmp(contents.data() + view.offset + adjustment, reinterpret_cast(first + 16u), 32u) == 0, + "the protected descriptor no longer reads the registered bytes"); + reader.WriteBack(); + } else if (exactWhole) { + Require(access.allocationAttempts() == attempts && probe.calls.empty() && window->lastUse > originalUse, + "accepting the stale exact whole import reallocated it or did not protect its returned pointer"); + const auto* kept = HostImportFor(range.importing, first, range.bytes); + Require(kept != nullptr && kept->buffer == originalBuffer && kept->ranges.size() == 1u && kept->ranges.front() == originalOwner && HostImportSerial(range.importing, first, range.bytes, false) == originalSerial, + "accepting the exact whole import changed its native or registered identity"); + } else { + Require(access.allocationAttempts() == attempts + 1u, "a discarded stale probe made an unheld prefix window block the whole import"); + const auto* whole = HostImportFor(range.importing, first, range.bytes); + Require(whole != nullptr && whole->base == first && whole->bytes == range.bytes && whole->ranges.size() == 1u && whole->ranges.front() == originalOwner, + "the stale-probe repair did not retain the exact registered owner over the whole import"); + Require(HostImportSerial(range.importing, first, range.bytes, false) != originalSerial, "replacing the unheld window reused its old import identity"); + Require(probe.calls.size() == 1u && probe.calls.front().pointer == reinterpret_cast(first) && probe.calls.front().bytes == range.bytes, + "the replacement native import did not reference the exact whole registered memory"); + Require(access.bytes(whole->buffer).size() == range.bytes, "the replacement import buffer does not cover its registered extent"); + } + Require(scratch == 0x12345678u && std::equal(originalGuest.begin(), originalGuest.end(), guest), "the import probe changed guest bytes or unrelated copied data"); + } +} + // An address-based build whose whole-range import does not fit the budget leaves the range's window alone: before, // every such build retired the window and the next access imported it again. void importRefusedWholeRangeTests(const Context& context) { @@ -1787,6 +1933,7 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) { spaceReadAcrossImportsWrittenTests(context); importJoinTests(context); importWholeRangeOverWindowTests(context); + importStaleProbeTests(context, access); importRefusedWholeRangeTests(context); importNeighbouringEdgesTests(context); importAtMapOverBudgetTests(context); From 84f72fc05731d1688ea65bd1e913194d15c2e673 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 01:57:53 +0400 Subject: [PATCH 025/131] docs(debt): record that guest GS accesses trap in Windows output --- docs/dev/debt/relinker.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index e5841ca308..27b53899d9 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -9,6 +9,7 @@ - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. - The length-changing path of the [instruction rewriter](../../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. - A `PT_SCE_RELRO` segment that no `PT_LOAD` covers, the PS4 layout, is [loaded](../../../core/relinker/relinker/src/parsing/ElfReader.cpp) as a read-write `PT_LOAD` and stays writable after relocation; FreeBSD rtld makes `PT_GNU_RELRO` read-only once relocation is done, and what the console does with `PT_SCE_RELRO` is not checked. +- In Windows output, a guest instruction that addresses memory through GS becomes `UD2` and NOP padding of its own length ([TLS builder](../../../core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp)): the host owns GS, and the guest GS base that FreeBSD's `sysarch(AMD64_SET_GSBASE)` would set is not emulated. A converted module runs until such an instruction is reached, which then faults. Prospero Win (PPSA99995) has six in its Wine `ntdll`. - In executables with a `PT_SCE_DYNLIBDATA` segment, the PS4 layout, the executable segment also holds read-only data after the code (CUSA00900: 37 MiB after its last function). The [relinker pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp) therefore scans for syscalls and import call sites only up to the end of the PLT that follows the last function of the unwind table; code after the PLT without an unwind entry is not scanned. `--to-intel` and the `unused-filter=1` and `unused-filter=2` analyses are not limited this way. - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP` to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). From 0ee7af6f5f75c3dcc5322f7e1b26ecc1b64d1b75 Mon Sep 17 00:00:00 2001 From: fdfawef324 Date: Wed, 14 Oct 2026 17:14:29 -0400 Subject: [PATCH 026/131] feat(agc): accept SAMPLE_STREAMOUTSTATS events without a destination in EVENT_WRITE EVENT_WRITE (PM4 opcode 0x46) with event type SAMPLE_STREAMOUTSTATS1-3 (0x01-0x03) or SAMPLE_STREAMOUTSTATS (0x20) failed submission validation with "EVENT_WRITE event type 1 is not implemented". Minecraft Preview (PPSA19639, discussion #1277, stop 9) submits a 2-dword type-1 packet while entering a world, and the GPU thread aborted intermittently. The four values are VGT_EVENT_TYPE entries in Linux drivers/gpu/drm/amd/include/navi10_enum.h L14155-14188 at 0fcb05b72344f40806cbe810de8872f1698bf053. drivers/gpu/drm/amd/amdgpu/nvd.h L326-347 at the same commit documents these events only with the sampling index (3, or EVENT_INDEX__SAMPLE_STREAMOUTSTATS* 8-11 on gfx10), followed by ADDRESS_LO/HI. The 2-dword index-0 packet has no destination dwords, and what the CP does with it is neither documented nor measured. AnyPS5 does not emulate the VGT stream-out counters, so there is nothing to sample. Packet execution already skips EVENT_WRITE. These event types are now accepted on the graphics queue as 2-dword packets with event index 0. The sampling indices 3 and 8-11 throw that stream-out statistics sampling to memory is not implemented, before the size and reserved-bit checks. Other indices, other sizes and the compute queue still throw. sceAgcDcbEventWrite ignores the address for every event type except 0x38 and 0x39, so a call with one of these event types and an address now builds a packet that validates and writes nothing. This is recorded in docs/dev/debt/libSceAgcDriver.md with the unmeasured hardware behaviour. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 11 +++++++++-- core/libs/prx/libSceAgcDriver/tests/Pm4.cpp | 18 ++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index bdee46ee16..5eb31653b5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -373,9 +373,11 @@ void Validate(std::span packet, std::uint32_t queue) { require((packet[1] & 0x7fffffffu) == 0 && (packet[2] & 0x7fffffffu) == 0, "CONTEXT_CONTROL register loading and shadowing are not implemented"); break; case 0x46: { - require((packet[1] & ~0x73fu) == 0, "unsupported EVENT_WRITE flags or reserved bits"); const auto eventType = packet[1] & 0x3fu; - const auto eventIndex = (packet[1] >> 8u) & 7u; + const auto eventIndex = (packet[1] >> 8u) & 0xfu; + const auto streamOutStatistics = (eventType >= 0x01u && eventType <= 0x03u) || eventType == 0x20u; + require(!streamOutStatistics || (eventIndex != 3 && (eventIndex < 8 || eventIndex > 11)), "stream-out statistics sampling to memory is not implemented"); + require((packet[1] & ~0x73fu) == 0, "unsupported EVENT_WRITE flags or reserved bits"); switch (eventType) { case 0x07: case 0x0f: case 0x10: size(2); @@ -396,6 +398,11 @@ void Validate(std::span packet, std::uint32_t queue) { size(2); require(eventIndex == 0, "invalid VGT_FLUSH event index"); break; + case 0x01: case 0x02: case 0x03: case 0x20: + graphics(); + size(2); + require(eventIndex == 0, "invalid stream-out statistics event index"); + break; case 0x39: graphics(); size(4); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 39131eb9ca..3414e86dbd 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -787,6 +787,22 @@ void testEventWrite() { expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x24}), 0x20); }, "compute queue"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x424}), 0); }, "VGT_FLUSH event index"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x1e, 0x1000, 0}), 0); }, "event type 30"); + for (const auto eventType : {0x01u, 0x02u, 0x03u, 0x20u}) { + AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType}), 0x20); }, "compute queue"); + for (std::uint32_t index = 1; index < 8; ++index) { + if (index == 3) continue; + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics event index"); + } + for (const auto index : {3u, 8u, 9u, 10u, 11u}) { + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType, 0x1000, 0x2}), 0); }, "stream-out statistics sampling to memory is not implemented"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "stream-out statistics sampling to memory is not implemented"); + } + for (const auto index : {12u, 13u, 14u, 15u}) { + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {(index << 8u) | eventType}), 0); }, "reserved bits"); + } + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {eventType, 0x1000, 0x2}), 0); }, "packet size"); + } } void testAcquireMem() { @@ -891,6 +907,8 @@ void testDriverSubmission() { makePacket(0x46, {0x2a}), makePacket(0x46, {0x72c}), makePacket(0x46, {0x2e}), + makePacket(0x46, {0x01}), + makePacket(0x46, {0x20}), makePacket(0x58, {0x02007fc0, 0, 0, 0, 0, 10, 0x200}), makePacket(0x58, {0x00800000, 0xffffffff, 0, 0, 0, 10}), makePacket(0x83, {0, 1, low(destination.data()), high(destination.data())}) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 7f3aa4d73c..d5b156766d 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -108,6 +108,7 @@ - [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. - [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. - [INDEX_ATTRIBUTES_INDIRECT](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (libSceAgcDriver, opcode 0x91) loads INDEX_BASE, INDEX_BUFFER_SIZE (in indices) and INDEX_TYPE from the 16-byte record at `attribute_base + attribute_index * 16`, read when the packet runs. The record layout (address low, address high, size, type) is PAL's [`IndexAttribIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9Chip.h#L60-L67) and the packet is the one [`BuildIndexAttributesIndirect`](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9CmdUtil.cpp#L2586-L2613) writes; neither is measured on a PS5 or seen in a title. `indirect_mode` 1 (`addr_offset`, which only PAL's ExecuteIndirect path emits) throws, and so does a record whose index type is above 3, as INDEX_TYPE does. - [16-bit RGBA color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) support the standard and the alternate (`COMP_SWAP` 1) component order. Alternate swaps red and blue in shader exports, write masks and constant-color blend factors, 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). The reverse orders (`COMP_SWAP` 2 and 3) keep throwing when blending, and simultaneous targets requiring conflicting blend constants throw. Console equivalence has not been measured on hardware. - [UINT16_ABGR color exports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - export format 7 is accepted only into an unsigned integer target, and each 16-bit value reaches the target unchanged; a value beyond a narrower target's range (`R8_UINT`) is undefined in Vulkan, and whether the console clamps or truncates it is not confirmed. SINT16_ABGR (8) throws From 9f96444c4a511d64fb7efd184dafe12de3fa66b9 Mon Sep 17 00:00:00 2001 From: fdfawef324 Date: Wed, 14 Oct 2026 17:12:13 -0400 Subject: [PATCH 027/131] feat(libSceNpManager): register NP state callbacks and report signed-out from sceNpCheckCallback sceNpRegisterStateCallback and sceNpRegisterStateCallbackA stored nothing, the unregister calls always succeeded and sceNpCheckCallback did nothing, so a title that waits for its NP state callback never learned the user's state. #1277 reports that Minecraft Preview (PPSA19639) needs the callback (stop 2). sceNpRegisterStateCallbackA now keeps up to 8 callbacks and returns the slot plus 1. A null callback returns INVALID_ARGUMENT, a callback that is already registered CALLBACK_ALREADY_REGISTERED and a ninth one CALLBACK_MAX (0x8055001D). sceNpUnregisterStateCallbackA returns INVALID_ARGUMENT for an id outside 1..8 and CALLBACK_NOT_REGISTERED for an empty slot. The legacy sceNpRegisterStateCallback keeps one callback with the same errors. The rules follow shadPS4 src/core/libraries/np/np_manager.cpp @15abf68c9b52 (RegisterStateCallbackA and UnregisterStateCallbackAById L857-L901, the legacy pair L981-L1005, ORBIS_NP_STATE_CALLBACK_MAX L36) and np_error.h L38. Each registration is marked pending. sceNpCheckCallback takes the pending registrations under the lock and calls them after releasing it, on the calling thread, with SIGNED_OUT for the initial user 0x10000000. For the A form this follows KytyPS5 src/libs/network.cpp @9e41d8320a27 (NpRegisterStateCallbackA L4021-L4034 marks the registration pending, NpCheckCallback L3959-L3974 delivers it once). KytyPS5 never calls the legacy callback (NpRegisterStateCallback and NpUnregisterStateCallback, L4009-L4019, do nothing). The legacy callback is called as (user, SIGNED_OUT, nullptr, userdata), as in shadPS4, which passes an NpId only for SIGNED_IN (L823, L938-L940). shadPS4 queues SIGNED_OUT from the user service login event instead (DispatchPendingNpStateCallbacks L903-L962), and the same event calls the reachability callback once with UNAVAILABLE (L922-L931, L959-L961). The reachability callback is still not called here. These differences and the unmeasured console behaviour are recorded in docs/dev/debt/libSceNpManager.md. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNpManager/Export.cpp | 68 +++++++++-- core/libs/tests/GuestNpManager.cpp | 141 +++++++++++++++++++++++ docs/dev/debt/libSceNpManager.md | 1 + 3 files changed, 203 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index a384c22023..0662f51026 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -2,6 +2,8 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include #include #include #include @@ -13,6 +15,7 @@ static constexpr int SCE_NP_ERROR_USER_NOT_FOUND = static_cast(0x80550007); static constexpr int SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED = static_cast(0x80550008); static constexpr int SCE_NP_ERROR_CALLBACK_NOT_REGISTERED = static_cast(0x80550009); static constexpr int SCE_NP_ERROR_REQUEST_NOT_FOUND = static_cast(0x80550014); +static constexpr int SCE_NP_ERROR_CALLBACK_MAX = static_cast(0x8055001D); static constexpr uint32_t NP_STATE_SIGNED_OUT = 1; static constexpr int NP_POLL_ASYNC_FINISHED = 0; static constexpr uint32_t NP_REACHABILITY_STATE_UNAVAILABLE = 0; @@ -32,6 +35,27 @@ int AddRequest() { liveRequests.insert(reqId); return reqId; } + +using NpStateCallbackA = void (APS5_VABI *)(int userId, uint32_t state, void* userdata); +using NpStateCallback = void (APS5_VABI *)(int userId, uint32_t state, NpId* npId, void* userdata); + +constexpr int NP_STATE_CALLBACK_MAX = 8; + +struct StateCallbackA { + NpStateCallbackA callback = nullptr; + void* userdata = nullptr; + bool pending = false; +}; + +struct StateCallback { + NpStateCallback callback = nullptr; + void* userdata = nullptr; + bool pending = false; +}; + +std::mutex stateMutex; +std::array stateCallbacksA; +StateCallback stateCallback; } extern "C" { @@ -42,6 +66,19 @@ int APS5_VABI sceNpAbortRequest(int req_id) { } int APS5_VABI sceNpCheckCallback(void) { + std::array callbacksA; + StateCallback legacy; + { + std::lock_guard lock(stateMutex); + callbacksA = stateCallbacksA; + legacy = stateCallback; + for (auto& entry : stateCallbacksA) entry.pending = false; + stateCallback.pending = false; + } + if (legacy.pending) legacy.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, nullptr, legacy.userdata); + for (const auto& entry : callbacksA) { + if (entry.pending) entry.callback(USER_SERVICE_INITIAL_USER_ID, NP_STATE_SIGNED_OUT, entry.userdata); + } return 0; } @@ -172,9 +209,11 @@ int APS5_VABI sceNpRegisterPremiumEventCallback(void* callback, void* userdata) return 0; } -int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata) { - (void)userdata; +int APS5_VABI sceNpRegisterStateCallback(NpStateCallback callback, void* userdata) { if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(stateMutex); + if (stateCallback.callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED; + stateCallback = {callback, userdata, true}; return 0; } @@ -208,6 +247,9 @@ int APS5_VABI sceNpSetTimeout(int reqId, int32_t resolveRetry, uint32_t resolveT } int APS5_VABI sceNpUnregisterStateCallback(void) { + std::lock_guard lock(stateMutex); + if (!stateCallback.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; + stateCallback = {}; return 0; } @@ -223,14 +265,26 @@ int APS5_VABI sceNpNotifyPremiumFeature(const void* param) { return 0; } -int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata) { - (void)userdata; +int APS5_VABI sceNpRegisterStateCallbackA(NpStateCallbackA callback, void* userdata) { if (!callback) return SCE_NP_ERROR_INVALID_ARGUMENT; - return 1; + std::lock_guard lock(stateMutex); + for (const auto& entry : stateCallbacksA) { + if (entry.callback == callback) return SCE_NP_ERROR_CALLBACK_ALREADY_REGISTERED; + } + for (int index = 0; index < NP_STATE_CALLBACK_MAX; ++index) { + if (stateCallbacksA[index].callback) continue; + stateCallbacksA[index] = {callback, userdata, true}; + return index + 1; + } + return SCE_NP_ERROR_CALLBACK_MAX; } -int APS5_VABI sceNpUnregisterStateCallbackA(int callback_id) { - (void)callback_id; +int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId) { + if (callbackId < 1 || callbackId > NP_STATE_CALLBACK_MAX) return SCE_NP_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(stateMutex); + auto& entry = stateCallbacksA[callbackId - 1]; + if (!entry.callback) return SCE_NP_ERROR_CALLBACK_NOT_REGISTERED; + entry = {}; return 0; } diff --git a/core/libs/tests/GuestNpManager.cpp b/core/libs/tests/GuestNpManager.cpp index eaa8a77d45..246c48367e 100644 --- a/core/libs/tests/GuestNpManager.cpp +++ b/core/libs/tests/GuestNpManager.cpp @@ -17,10 +17,22 @@ int APS5_VABI sceNpSetTimeout(int reqId, std::int32_t resolveRetry, std::uint32_ std::uint32_t connTimeout, std::uint32_t sendTimeout, std::uint32_t recvTimeout); } +extern "C" { +int APS5_VABI sceNpCheckCallback(void); +int APS5_VABI sceNpRegisterStateCallback(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterStateCallback(void); +int APS5_VABI sceNpRegisterStateCallbackA(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterStateCallbackA(int callbackId); +int APS5_VABI sceNpRegisterNpReachabilityStateCallback(void* callback, void* userdata); +int APS5_VABI sceNpUnregisterNpReachabilityStateCallback(void); +} + namespace { constexpr int InvalidArgument = static_cast(0x80550003u); constexpr int SignedOut = static_cast(0x80550006u); +constexpr int AlreadyRegistered = static_cast(0x80550008u); +constexpr int NotRegistered = static_cast(0x80550009u); constexpr int RequestNotFound = static_cast(0x80550014u); void Require(bool condition, const char* message) { @@ -30,6 +42,133 @@ void Require(bool condition, const char* message) { } } +struct StateCall { + int calls; + int userId; + std::uint32_t state; + NpId* npId; + void* userdata; +}; + +constexpr int InitialUserId = 0x10000000; +constexpr std::uint32_t StateSignedOut = 1; +StateCall stateCallsA[9]; +StateCall legacyStateCall; + +template +void APS5_VABI StateCallbackA(int userId, std::uint32_t state, void* userdata) { + stateCallsA[Index] = {stateCallsA[Index].calls + 1, userId, state, nullptr, userdata}; +} + +void APS5_VABI LegacyStateCallback(int userId, std::uint32_t state, NpId* npId, void* userdata) { + legacyStateCall = {legacyStateCall.calls + 1, userId, state, npId, userdata}; +} + +bool SignedOutOnce(const StateCall& call, void* userdata) { + return call.calls == 1 && call.userId == InitialUserId && call.state == StateSignedOut && + call.npId == nullptr && call.userdata == userdata; +} + +void APS5_VABI AbortingReachabilityCallback(int, std::uint32_t, void*) { + std::fprintf(stderr, "NpManager: the reachability callback was called\n"); + std::abort(); +} + +struct SelfUnregister { + int id; + int calls; + int result; +}; + +void APS5_VABI SelfUnregisteringCallback(int, std::uint32_t, void* userdata) { + auto* context = static_cast(userdata); + ++context->calls; + context->result = sceNpUnregisterStateCallbackA(context->id); +} + +void CheckStateCallbacksA() { + constexpr int CallbackMax = static_cast(0x8055001Du); + void* const callbacks[] = { + reinterpret_cast(&StateCallbackA<0>), reinterpret_cast(&StateCallbackA<1>), + reinterpret_cast(&StateCallbackA<2>), reinterpret_cast(&StateCallbackA<3>), + reinterpret_cast(&StateCallbackA<4>), reinterpret_cast(&StateCallbackA<5>), + reinterpret_cast(&StateCallbackA<6>), reinterpret_cast(&StateCallbackA<7>), + reinterpret_cast(&StateCallbackA<8>), + }; + int userdata[9]{}; + Require(sceNpRegisterStateCallbackA(nullptr, &userdata[0]) == InvalidArgument, "null state callback"); + int ids[8]{}; + ids[0] = sceNpRegisterStateCallbackA(callbacks[0], &userdata[0]); + Require(ids[0] >= 1 && ids[0] <= 8, "state callback id outside 1..8"); + Require(sceNpRegisterStateCallbackA(callbacks[0], &userdata[1]) == AlreadyRegistered, "duplicate state callback"); + Require(stateCallsA[0].calls == 0, "registering calls the state callback"); + Require(sceNpRegisterNpReachabilityStateCallback(reinterpret_cast(&AbortingReachabilityCallback), nullptr) == 0, + "registering the reachability callback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed"); + Require(SignedOutOnce(stateCallsA[0], &userdata[0]), "sceNpCheckCallback must deliver SIGNED_OUT once"); + Require(sceNpUnregisterNpReachabilityStateCallback() == 0, "unregistering the reachability callback"); + Require(sceNpCheckCallback() == 0, "second sceNpCheckCallback failed"); + Require(stateCallsA[0].calls == 1, "the second sceNpCheckCallback delivered the state again"); + for (int index = 1; index < 8; ++index) { + ids[index] = sceNpRegisterStateCallbackA(callbacks[index], &userdata[index]); + Require(ids[index] >= 1 && ids[index] <= 8, "state callback id outside 1..8"); + for (int other = 0; other < index; ++other) { + Require(ids[index] != ids[other], "two state callbacks got the same id"); + } + } + Require(sceNpRegisterStateCallbackA(callbacks[8], &userdata[8]) == CallbackMax, "ninth state callback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with eight callbacks"); + for (int index = 0; index < 8; ++index) { + Require(SignedOutOnce(stateCallsA[index], &userdata[index]), "each callback gets SIGNED_OUT once"); + } + Require(stateCallsA[8].calls == 0, "a rejected callback was called"); + Require(sceNpUnregisterStateCallbackA(0) == InvalidArgument, "callback id 0"); + Require(sceNpUnregisterStateCallbackA(9) == InvalidArgument, "callback id 9"); + Require(sceNpUnregisterStateCallbackA(ids[3]) == 0, "unregistering one state callback"); + for (int index = 0; index < 8; ++index) { + if (index == 3) continue; + Require(sceNpRegisterStateCallbackA(callbacks[index], &userdata[index]) == AlreadyRegistered, + "unregistering one id removed another callback"); + } + Require(sceNpRegisterStateCallbackA(callbacks[3], &userdata[3]) == ids[3], "registering again takes the freed slot"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed after registering again"); + for (int index = 0; index < 8; ++index) { + Require(stateCallsA[index].calls == (index == 3 ? 2 : 1), "only the registration made again gets SIGNED_OUT"); + } + for (int index = 0; index < 8; ++index) { + Require(sceNpUnregisterStateCallbackA(ids[index]) == 0, "unregistering a state callback"); + } + Require(sceNpUnregisterStateCallbackA(ids[0]) == NotRegistered, "unregistering a state callback twice"); + const int removed = sceNpRegisterStateCallbackA(callbacks[8], &userdata[8]); + Require(removed >= 1 && removed <= 8, "a freed slot is reused"); + Require(sceNpUnregisterStateCallbackA(removed) == 0, "unregistering before sceNpCheckCallback"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with no callbacks"); + Require(stateCallsA[8].calls == 0, "an unregistered callback was called"); + SelfUnregister self{}; + self.id = sceNpRegisterStateCallbackA(reinterpret_cast(&SelfUnregisteringCallback), &self); + Require(self.id >= 1 && self.id <= 8, "state callback id outside 1..8"); + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with a callback that unregisters itself"); + Require(self.calls == 1 && self.result == 0, "a state callback must be able to unregister itself"); + Require(sceNpUnregisterStateCallbackA(self.id) == NotRegistered, "the callback did not unregister itself"); +} + +void CheckLegacyStateCallback() { + int userdata = 0; + NpId sentinel{}; + auto* callback = reinterpret_cast(&LegacyStateCallback); + Require(sceNpUnregisterStateCallback() == NotRegistered, "unregistering with no legacy callback"); + Require(sceNpRegisterStateCallback(nullptr, &userdata) == InvalidArgument, "null legacy callback"); + Require(sceNpRegisterStateCallback(callback, &userdata) == 0, "registering the legacy callback"); + Require(sceNpRegisterStateCallback(callback, nullptr) == AlreadyRegistered, "duplicate legacy callback"); + legacyStateCall.npId = &sentinel; + Require(sceNpCheckCallback() == 0, "sceNpCheckCallback failed with the legacy callback"); + Require(SignedOutOnce(legacyStateCall, &userdata), "the legacy callback must get SIGNED_OUT with no NpId"); + Require(sceNpCheckCallback() == 0, "second sceNpCheckCallback failed with the legacy callback"); + Require(legacyStateCall.calls == 1, "the legacy callback got the state again"); + Require(sceNpUnregisterStateCallback() == 0, "unregistering the legacy callback"); + Require(sceNpUnregisterStateCallback() == NotRegistered, "unregistering the legacy callback twice"); +} + void CheckRequestTimeouts() { Require(sceNpSetTimeout(0x7fffffff, 2, 0, 0, 0, 0) == RequestNotFound, "never created request"); Require(sceNpSetTimeout(0x7fffffff, 0, 0, 0, 0, 0) == InvalidArgument, "arguments checked before the request"); @@ -59,6 +198,8 @@ void CheckRequestTimeouts() { } int main() { + CheckStateCallbacksA(); + CheckLegacyStateCallback(); CheckRequestTimeouts(); NpId npId{}; std::memset(&npId, 0x5a, sizeof(npId)); diff --git a/docs/dev/debt/libSceNpManager.md b/docs/dev/debt/libSceNpManager.md index cb0212dd00..18d9bc1995 100644 --- a/docs/dev/debt/libSceNpManager.md +++ b/docs/dev/debt/libSceNpManager.md @@ -9,3 +9,4 @@ - [NP reachability callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) retain one callback and its user data; duplicate registration and unregistering an absent callback return the errors used by [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/libraries/np/np_manager.cpp). No callback is delivered because the existing NP model remains signed out with reachability unavailable. - [sceNpCheckNpAvailability](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the `(req_id, online_id)` signature and the errors follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/libraries/np/np_manager.cpp) with no user signed in to PSN: a null online id is `SCE_NP_ERROR_INVALID_ARGUMENT`, and any other online id matches no user, `SCE_NP_ERROR_USER_NOT_FOUND`; the request id is not checked, as in the other NpManager requests +- [NP state callbacks](../../../core/libs/prx/libSceNpManager/Export.cpp) (libSceNpManager) - the registration rules are [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L857-L901)'s: `sceNpRegisterStateCallbackA` keeps up to 8 callbacks and returns the slot plus 1, a null callback returns `INVALID_ARGUMENT`, a callback already registered `CALLBACK_ALREADY_REGISTERED` and a ninth one `CALLBACK_MAX` (0x8055001D, [np_error.h](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_error.h#L38)); `sceNpUnregisterStateCallbackA` returns `INVALID_ARGUMENT` for an id outside 1..8 and `CALLBACK_NOT_REGISTERED` for an empty slot; the legacy [sceNpRegisterStateCallback](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L981-L1005) keeps one callback with the same errors. Each registration gets `SIGNED_OUT` for the initial user (0x10000000) once, from the next `sceNpCheckCallback` on the calling thread. For the A form this follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3959-L3974), whose [sceNpRegisterStateCallbackA](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4021-L4034) marks the registration pending. KytyPS5 never calls the legacy callback ([sceNpRegisterStateCallback](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L4009-L4019) stores nothing); it is called as `(user, SIGNED_OUT, nullptr, userdata)`, as in shadPS4, which passes an NpId only for `SIGNED_IN` ([L823](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L823), [L938-L940](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L938-L940)). shadPS4 instead queues `SIGNED_OUT` from the user service login event and delivers it from `sceNpCheckCallback` ([DispatchPendingNpStateCallbacks](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L903-L962)); the reachability callback is not given an initial `UNAVAILABLE`, unlike that function ([L922-L961](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/np/np_manager.cpp#L922-L961)). A callback being delivered when another thread unregisters it is still called once. When and how often the console calls the state callbacks, and the errors, are not measured From 7a86e5ae32655bf0a70d8eff35167e28f8a860bc Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 02:15:26 +0400 Subject: [PATCH 028/131] feat(libSceVideoOut): export the hardware cursor functions as not implemented sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress and sceVideoOutCursorSetPosition are imported by Prospero Win's (PPSA99995) Vulkan module, so on Windows, where imports are bound at load, the title cannot load without them. They report that they are not implemented when called. --- .../libs/prx/libSceVideoOut/Unimplemented.cpp | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/libs/prx/libSceVideoOut/Unimplemented.cpp b/core/libs/prx/libSceVideoOut/Unimplemented.cpp index ef58c55add..2205809941 100644 --- a/core/libs/prx/libSceVideoOut/Unimplemented.cpp +++ b/core/libs/prx/libSceVideoOut/Unimplemented.cpp @@ -17,6 +17,26 @@ int APS5_VABI sceVideoOutClearFlipMaster() { return 0; } +int APS5_VABI sceVideoOutCursorEnable(int32_t, int32_t, const void*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorDisable(int32_t, int32_t) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorSetImageAddress(int32_t, int32_t, const void*) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVideoOutCursorSetPosition(int32_t, int32_t, uint32_t, uint32_t) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceVideoOutAdjustColor_(int handle, const VideoOutColorSettings* settings, uint32_t settings_size) { if (settings_size < sizeof(VideoOutColorSettings)) throw std::runtime_error("sceVideoOutAdjustColor_: VIDEO_OUT_ERROR_INVALID_VALUE"); return sceVideoOutAdjustColor(handle, settings); From e8d32bbc2db839aaf4de703073fde971c35f3b4a Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 02:16:48 +0400 Subject: [PATCH 029/131] test(libSceVideoOut): check that the cursor functions report they are not implemented guest_video_out_cursor calls each of the four cursor functions with a handle, a cursor index, an image and a position, and requires the "not implemented" error. Without the exports it does not link. --- core/libs/prx/libSceVideoOut/CMakeLists.txt | 10 ++++++ core/libs/tests/GuestVideoOutCursor.cpp | 35 +++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 core/libs/tests/GuestVideoOutCursor.cpp diff --git a/core/libs/prx/libSceVideoOut/CMakeLists.txt b/core/libs/prx/libSceVideoOut/CMakeLists.txt index e216b43fb4..08c199ab5c 100644 --- a/core/libs/prx/libSceVideoOut/CMakeLists.txt +++ b/core/libs/prx/libSceVideoOut/CMakeLists.txt @@ -71,6 +71,16 @@ if(BUILD_TESTING) set_tests_properties(guest_video_out_high_frame_rate PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_video_out_cursor_tests ${LIBS_INCLUDE_DIR}/tests/GuestVideoOutCursor.cpp) + target_include_directories(guest_video_out_cursor_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_video_out_cursor_tests PRIVATE libSceVideoOut libkernel libc) + configure_windows_unwind(guest_video_out_cursor_tests) + add_test(NAME guest_video_out_cursor COMMAND guest_video_out_cursor_tests) + set_tests_properties(guest_video_out_cursor PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_video_out_cursor PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() set(ANYPS5_VIDEO_OUT_TEST_DRIVER "offscreen" CACHE STRING "SDL video driver for VideoOut flip tests") diff --git a/core/libs/tests/GuestVideoOutCursor.cpp b/core/libs/tests/GuestVideoOutCursor.cpp new file mode 100644 index 0000000000..214293bdd2 --- /dev/null +++ b/core/libs/tests/GuestVideoOutCursor.cpp @@ -0,0 +1,35 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVideoOutCursorEnable(std::int32_t handle, std::int32_t index, const void* address); +int APS5_VABI sceVideoOutCursorDisable(std::int32_t handle, std::int32_t index); +int APS5_VABI sceVideoOutCursorSetImageAddress(std::int32_t handle, std::int32_t index, const void* address); +int APS5_VABI sceVideoOutCursorSetPosition(std::int32_t handle, std::int32_t index, std::uint32_t x, std::uint32_t y); +} + +static bool ReportsNotImplemented(const std::string& name, const std::function& call) { + try { + call(); + } catch (const std::runtime_error& error) { + if (error.what() == name + " not implemented") return true; + std::fprintf(stderr, "%s threw: %s\n", name.c_str(), error.what()); + return false; + } + std::fprintf(stderr, "%s returned instead of reporting that it is not implemented\n", name.c_str()); + return false; +} + +int main() { + static const std::uint32_t image[64 * 64] = {}; + bool passed = ReportsNotImplemented("sceVideoOutCursorEnable", [] { return sceVideoOutCursorEnable(1, 0, image); }); + passed = ReportsNotImplemented("sceVideoOutCursorSetImageAddress", [] { return sceVideoOutCursorSetImageAddress(1, 0, image); }) && passed; + passed = ReportsNotImplemented("sceVideoOutCursorSetPosition", [] { return sceVideoOutCursorSetPosition(1, 0, 960, 540); }) && passed; + passed = ReportsNotImplemented("sceVideoOutCursorDisable", [] { return sceVideoOutCursorDisable(1, 0); }) && passed; + return passed ? 0 : 1; +} From b36f6f32602059c2975de146f62296900e902d7f Mon Sep 17 00:00:00 2001 From: YatoVoid Date: Wed, 14 Oct 2026 17:22:06 -0500 Subject: [PATCH 030/131] test(agc): disable the shader disk cache in agc_shader_memory_tests --- core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 261c21e612..1239ee3ab3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4932,6 +4932,11 @@ void verifySrgbDepthCompareModes() { int main(int argc, char** argv) { try { +#ifdef _WIN32 + _putenv_s("ANYPS5_NO_SHADER_CACHE", "1"); +#else + setenv("ANYPS5_NO_SHADER_CACHE", "1", 1); +#endif using namespace ShaderRecompiler; if (argc == 2 && std::string_view(argv[1]) == "--frontend-pair") { verifyFrontendPair(); From 62b09018d4676bf385d4744e982f13e330416359 Mon Sep 17 00:00:00 2001 From: ArjenS-LA Date: Wed, 14 Oct 2026 15:36:25 -0700 Subject: [PATCH 031/131] fix(relinker): reject needed companion output collisions --- core/relinker/main.cpp | 1 + .../relinker/tests/test_output_collisions.py | 32 ++++++++++++++++++- .../tests/test_secondary_output_collisions.py | 3 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 6b967356a5..1f143afb7f 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -290,6 +290,7 @@ int Run(const int argc, char* argv[]) { for (const auto& executable : executables) outputPaths.push_back(Io::NativePath(executable.OutputPath)); for (const auto& artifact : guestArtifacts) outputPaths.push_back(artifact.Path); outputPaths.insert(outputPaths.end(), registryPaths.begin(), registryPaths.end()); + outputPaths.insert(outputPaths.end(), neededPaths.begin(), neededPaths.end()); if (desktopEntry) { outputPaths.push_back(desktopEntry->IconPath); outputPaths.push_back(desktopEntry->EntryPath); diff --git a/core/relinker/relinker/tests/test_output_collisions.py b/core/relinker/relinker/tests/test_output_collisions.py index 2026412310..16466abcbe 100644 --- a/core/relinker/relinker/tests/test_output_collisions.py +++ b/core/relinker/relinker/tests/test_output_collisions.py @@ -25,6 +25,8 @@ def prepare(case, windows): "module": case / "app0/sce_module/sample.prx.guest.prx", "registry": case / "output.registry.json", "module-registry": case / "output.sample.prx.guest.prx.registry.json", + "needed": case / "output.needed.json", + "module-needed": case / "output.sample.prx.guest.prx.needed.json", } if windows: paths["diagnostics"] = case / "windows-diagnostics-imports.txt" @@ -53,6 +55,34 @@ def main(): checked = 0 with tempfile.TemporaryDirectory(prefix="anyps5-output-collisions-") as directory: work = Path(directory) + for windows, existing in itertools.product((False, True), repeat=2): + case = work / f"needed-secondary-{windows}-{existing}" + case.mkdir() + source = case / "input.elf" + secondary = case / "output.needed" + source.write_bytes(main_fixture()) + secondary.write_bytes(main_fixture()) + module = case / "sce_module/sample.prx" + module.parent.mkdir() + module.write_bytes(guest_fixture(PLAIN_SITE)) + outputs = [case / name for name in ("output.json", "output.needed.json", "output.registry.json", + "output.needed.registry.json", "output.needed.needed.json", + "app0/sce_module/sample.prx.guest.prx", + "output.sample.prx.guest.prx.registry.json", + "output.sample.prx.guest.prx.needed.json")] + if existing: + for path in outputs: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(b"previous output") + result = subprocess.run([str(relinker), *(["--windows"] if windows else []), "--registry", + "--executable", secondary.name, str(source), str(outputs[0])], cwd=case, + capture_output=True, text=True, timeout=20) + assert result.returncode == 2 and "Output paths collide" in result.stderr, ( + windows, existing, result.returncode, result.stdout, result.stderr) + for path in outputs: + assert path.read_bytes() == b"previous output" if existing else not path.exists(), path + assert source.read_bytes() == secondary.read_bytes() == main_fixture() + assert module.read_bytes() == guest_fixture(PLAIN_SITE) for windows in (False, True): command, paths, inputs = prepare(work / f"{windows}-control", windows) links = paths["executable"].parent / "separate-links" @@ -65,7 +95,7 @@ def main(): assert result.returncode == 0, (result.stdout, result.stderr) for name in ("executable", "module"): assert paths[name].read_bytes().startswith(b"MZ" if windows else b"\x7fELF") - for name in ("registry", "module-registry"): + for name in ("registry", "module-registry", "needed", "module-needed"): json.loads(paths[name].read_text()) for path, original in inputs.items(): assert path.read_bytes() == original diff --git a/core/relinker/relinker/tests/test_secondary_output_collisions.py b/core/relinker/relinker/tests/test_secondary_output_collisions.py index a0db558788..070536466c 100644 --- a/core/relinker/relinker/tests/test_secondary_output_collisions.py +++ b/core/relinker/relinker/tests/test_secondary_output_collisions.py @@ -20,6 +20,7 @@ def prepare_secondary(case, windows): options.extend(["--executable", source.name]) paths[name] = case / f"{source.name}{paths['executable'].suffix}" paths[f"{name}-registry"] = case / f"{source.name}.registry.json" + paths[f"{name}-needed"] = case / f"{source.name}.needed.json" if not windows: icon = case / "sce_sys/icon0.png" inputs[icon] = icon.read_bytes() @@ -46,7 +47,7 @@ def main(): for name in ("executable", "module", *sorted(secondary)): assert paths[name].read_bytes().startswith(b"MZ" if windows else b"\x7fELF"), paths[name] for name, path in paths.items(): - if "registry" in name: + if "registry" in name or "needed" in name: json.loads(path.read_text()) for path, original in inputs.items(): assert path.read_bytes() == original, path From 10bc2f05aff0a57e64a4e71a3195d59416e0da43 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 02:46:21 +0400 Subject: [PATCH 032/131] docs(debt): record where the cursor functions' signatures come from --- docs/dev/debt/libSceVideoOut.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceVideoOut.md b/docs/dev/debt/libSceVideoOut.md index 248855971a..f6f21f6918 100644 --- a/docs/dev/debt/libSceVideoOut.md +++ b/docs/dev/debt/libSceVideoOut.md @@ -9,6 +9,7 @@ ### Unknown function info - [sceVideoOutDeleteVrrActiveStatusEvent](../../../core/libs/prx/libSceVideoOut/src/Event.cpp) (libSceVideoOut, NID `-q-vbO859Tw`) - the name hashes to the NID PPSA02530 imports and is listed in [sce_symbols](https://github.com/zecoxao/sce_symbols) `aerolib.csv` at `fe2cc2948e`. The signature `(eq, handle)` and the behaviour, removing the handle's VRR status registration from the queue as the other `Delete*Event` calls do, follow sceVideoOutAddVrrActiveStatusEvent; no title has been observed calling it +- [sceVideoOutCursorEnable, sceVideoOutCursorDisable, sceVideoOutCursorSetImageAddress, sceVideoOutCursorSetPosition](../../../core/libs/prx/libSceVideoOut/Unimplemented.cpp) (libSceVideoOut, NIDs `UPZWCH8qTM4`, `fEPEwROdCwM`, `u-9bhWQApq8`, `N17AK88NdD0`) - the names hash to the NIDs Prospero Win (PPSA99995) imports, and the parameter lists `(handle, index, address)`, `(handle, index)`, `(handle, index, address)` and `(handle, index, x, y)` follow its [declarations](https://github.com/mpereiraesaa/prospero-win/blob/644e32e400c555e340d507383398c5d21b39b0b0/wine/ps5/pw_vulkan_radv.c#L65-L68). The cursor image's format and size and the console's return values are not known; the functions report that they are not implemented. - [sceVideoOutGetOutputStatusWithVrr](../../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut, NID `gWT7X8H0bYs`) - `(int handle, void* status)`, the 80-byte layout and the return codes follow the clean-room description in #2746. Bytes 0-47 are `sceVideoOutGetOutputStatus`'s; bytes 48-53, which the console copies from an internal query whose fields are not established, are written as zero, as are the rest, so the 16-bit value at byte 52 never adjusts `refreshRate`. A null `status` returns `0x80290002` and an unknown handle `0x8029000B`. The console's earlier global-flag check (`0x8029001C`) and its object-field check (`0x8029001B`) test states that are not modelled and are never reported. - [sceVideoOutRegisterBuffers2](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted - [sceVideoOutAddBufferAttributeOption](../../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut, NID `MBlv8YWoyOk`) - the signature `(VideoOutBufferAttribute2*, uint64_t option)` comes from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/VideoOut/VideoOut.cs#L174-L176) and the call site in PPSA25380, which passes 0x20 and ignores the result. The option is ORed into `attribute->option`; whether the console validates the bits, keeps them elsewhere or fails on an unknown option is unknown. A null attribute throws. Option 0x20 makes `sceVideoOutRegisterBuffers2` throw, as every option other than 0 and 8; what it selects for an overlay buffer is unknown From 3e98703fc8ca943e5c7f5dd494245aa3f8c694b5 Mon Sep 17 00:00:00 2001 From: yripper Date: Tue, 13 Oct 2026 02:20:31 -0300 Subject: [PATCH 033/131] feat(libc): reserve the guest arena on macOS macOS has no /proc/self/maps or MAP_FIXED_NOREPLACE, so the arena never came up and every guest allocation threw. Enumerate host regions with mach_vm_region and reserve free chunks with a fixed mach_vm_map, which fails instead of replacing. This skips dyld, the Rosetta runtime, and ranges the kernel never lets a process map (0x10_0000_0000-0x70_0000_0000). The POSIX commit/protection paths now build on macOS unchanged. Adds standalone guest_arena tests (x86_64 under Rosetta and arm64). --- core/libs/CMakeLists.txt | 1 + core/libs/prx/libc/src/GuestArena.cpp | 68 +++++++++-- core/libs/tests/GuestArenaHost.cpp | 125 +++++++++++++++++++++ core/libs/tests/guest_arena/CMakeLists.txt | 21 ++++ 4 files changed, 204 insertions(+), 11 deletions(-) create mode 100644 core/libs/tests/GuestArenaHost.cpp create mode 100644 core/libs/tests/guest_arena/CMakeLists.txt diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 8385138261..6a4c4aa131 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -257,6 +257,7 @@ if(BUILD_TESTING) endif() if(APPLE) add_subdirectory(tests/write_watch) + add_subdirectory(tests/guest_arena) endif() if(WIN32) add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp) diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index ca580fc5bd..008ad0da76 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -11,6 +11,10 @@ #include #include #endif +#ifdef __APPLE__ +#include +#include +#endif #include #include #include @@ -148,8 +152,8 @@ public: private: Arena() { -#if defined(__linux__) - SeedLinuxUsedRanges(); +#if defined(__linux__) || defined(__APPLE__) + SeedHostUsedRanges(); #elif defined(_WIN32) _writeWatched = std::getenv("APS5_NO_WRITE_WATCH") == nullptr; SYSTEM_INFO system{}; @@ -182,7 +186,8 @@ private: } #if defined(__linux__) - void SeedLinuxUsedRanges() { + static std::vector> HostMappings() { + std::vector> mappings; std::ifstream maps("/proc/self/maps"); if (!maps.is_open()) throw std::runtime_error("guest arena: cannot open /proc/self/maps"); std::string line; @@ -191,6 +196,51 @@ private: std::uintptr_t begin = 0, end = 0; char dash = 0; if (!(fields >> std::hex >> begin >> dash >> end) || dash != '-' || end <= begin) throw std::runtime_error("guest arena: malformed /proc/self/maps line: " + line); + mappings.emplace_back(begin, end); + } + return mappings; + } + + static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) { + void* const wanted = reinterpret_cast(address); + void* const placed = mmap(wanted, bytes, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0); + if (placed == wanted) return 0; + if (placed == MAP_FAILED) return errno; + munmap(placed, bytes); + return EINVAL; + } +#elif defined(__APPLE__) + // macOS has no /proc/self/maps; dyld, the Rosetta runtime and ranges the kernel never lets a process + // map (max protection none, e.g. 0x10_0000_0000-0x70_0000_0000) all sit inside the PS5 application map. + static std::vector> HostMappings() { + std::vector> mappings; + mach_vm_address_t address = ArenaStart; + while (address < ApplicationAreaEnd) { + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + const kern_return_t result = mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object); + if (result == KERN_INVALID_ADDRESS) break; + if (result != KERN_SUCCESS) throw std::runtime_error("guest arena: mach_vm_region failed"); + mappings.emplace_back(static_cast(address), static_cast(address + size)); + address += size; + } + return mappings; + } + + // MAP_FIXED_NOREPLACE does not exist on macOS; a fixed mach_vm_map fails instead of replacing. + static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) { + mach_vm_address_t wanted = address; + const kern_return_t result = mach_vm_map(mach_task_self(), &wanted, bytes, 0, VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_NONE, VM_PROT_ALL, VM_INHERIT_DEFAULT); + if (result == KERN_SUCCESS) return 0; + return result == KERN_NO_SPACE ? EEXIST : ENOMEM; + } +#endif + +#if defined(__linux__) || defined(__APPLE__) + void SeedHostUsedRanges() { + for (const auto& [begin, end] : HostMappings()) { if (end <= ArenaStart || begin >= ApplicationAreaEnd) continue; const auto first = std::max(begin, ArenaStart); const auto last = std::min(end, ApplicationAreaEnd); @@ -208,12 +258,8 @@ private: constexpr std::uintptr_t kChunk = 1ULL << 30; for (std::uintptr_t cursor = from; cursor < to; cursor += kChunk) { const auto end = std::min(cursor + kChunk, to); - void* const wanted = reinterpret_cast(cursor); - void* const placed = mmap(wanted, end - cursor, PROT_NONE, - MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE | MAP_FIXED_NOREPLACE, -1, 0); - if (placed == wanted) continue; - const int failed = placed == MAP_FAILED ? errno : EINVAL; - if (placed != MAP_FAILED) munmap(placed, end - cursor); + const int failed = ReserveNoReplace(cursor, end - cursor); + if (failed == 0) continue; if (failed != EEXIST) { char message[128]; std::snprintf(message, sizeof(message), "guest arena: cannot reserve 0x%llx+0x%llx", @@ -428,7 +474,7 @@ void GuestArenaForgetSection_nid_postfix(void* section) { } #endif -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) namespace { int PosixProtection(std::uint32_t windowsProtection) { switch (windowsProtection) { @@ -449,7 +495,7 @@ void* PlaceAt(void* pointer, std::size_t bytes, std::uint32_t protection) { } #endif -#if defined(__linux__) +#if defined(__linux__) || defined(__APPLE__) void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) { if (!Arena::Get().Contains(reinterpret_cast(address), bytes)) throw OutsideArena("set protection", reinterpret_cast(address), bytes); if (mprotect(reinterpret_cast(address), bytes, PosixProtection(protection)) != 0) diff --git a/core/libs/tests/GuestArenaHost.cpp b/core/libs/tests/GuestArenaHost.cpp new file mode 100644 index 0000000000..6378a14180 --- /dev/null +++ b/core/libs/tests/GuestArenaHost.cpp @@ -0,0 +1,125 @@ +#include "prx/libc/include/GuestArena.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#if !defined(__APPLE__) +#error These tests cover the macOS guest arena reservation +#endif + +using namespace GuestArena; + +namespace { + +constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; +constexpr std::uintptr_t ApplicationAreaEnd = 0x000000FC00000000ull; +constexpr std::uint32_t ProtectionNoAccess = 0x01; +constexpr std::uint32_t ProtectionReadOnly = 0x02; +constexpr std::uint32_t ProtectionReadWrite = 0x04; + +struct HostRegion { + std::uintptr_t start; + std::uintptr_t end; + vm_prot_t maxProtection; +}; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::vector RegionsInArenaRange() { + std::vector regions; + mach_vm_address_t address = ArenaStart; + while (address < ApplicationAreaEnd) { + mach_vm_size_t size = 0; + vm_region_basic_info_data_64_t info{}; + mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; + mach_port_t object = MACH_PORT_NULL; + if (mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object) != KERN_SUCCESS) break; + if (address >= ApplicationAreaEnd) break; + regions.push_back({static_cast(address), static_cast(address + size), info.max_protection}); + address += size; + } + return regions; +} + +// The kernel keeps ranges inside the PS5 application map that no process may map (max protection none); +// the arena has to come up around them instead of giving up. +void CheckAvailable() { + Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); + std::uintptr_t base = 0; + std::size_t bytes = 0; + GuestArenaRange_nid_postfix(&base, &bytes); + Require(base == ArenaStart, "the guest arena does not start at the PS5 application map base"); + Require(base + bytes == ApplicationAreaEnd, "the guest arena does not end at the PS5 application map limit"); +} + +void CheckSkipsUnmappable() { + for (const auto& region : RegionsInArenaRange()) { + if (region.maxProtection != VM_PROT_NONE || region.end - region.start < 0x10000) continue; + void* placed = GuestArenaAllocateAtOrAbove_nid_postfix(region.start, 0x10000, 0x10000); + const auto address = reinterpret_cast(placed); + Require(address >= region.end, "an allocation landed inside a range the kernel never lets the process map"); + GuestArenaRelease_nid_postfix(placed, 0x10000); + return; + } + std::puts("no unmappable range inside the arena on this host; nothing to skip"); +} + +void CheckHostRegionsKept() { + std::size_t hostRegions = 0; + for (const auto& region : RegionsInArenaRange()) { + // Reservations made by the arena itself are no-access with full max protection. + if (region.maxProtection == VM_PROT_ALL) continue; + ++hostRegions; + bool rejected = false; + try { + GuestArenaMarkUsed_nid_postfix(reinterpret_cast(region.start), region.end - region.start); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected, "a fixed guest mapping over a host region was accepted"); + } + std::printf("%zu host regions inside the arena range are protected\n", hostRegions); +} + +void CheckCommitAndProtect() { + constexpr std::size_t bytes = 0x10000; + auto* pointer = static_cast(GuestArenaAllocate_nid_postfix(bytes, bytes)); + GuestArenaCommit_nid_postfix(pointer, bytes, ProtectionReadWrite, bytes); + pointer[0] = 0x5a; + pointer[bytes - 1] = 0xa5; + Require(pointer[0] == 0x5a && pointer[bytes - 1] == 0xa5, "committed guest memory does not keep writes"); + GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, ProtectionReadOnly); + Require(pointer[0] == 0x5a, "read-only guest memory lost its contents"); + GuestArenaReset_nid_postfix(pointer, bytes); + GuestArenaSetProtection_nid_postfix(reinterpret_cast(pointer), bytes, ProtectionNoAccess); + GuestArenaRelease_nid_postfix(pointer, bytes); +} + +} + +int main(int argc, char** argv) { + try { + Require(argc == 2, "expected available, unmappable, host or commit"); + const std::string_view mode(argv[1]); + if (mode == "available") CheckAvailable(); + else if (mode == "unmappable") CheckSkipsUnmappable(); + else if (mode == "host") CheckHostRegionsKept(); + else if (mode == "commit") CheckCommitAndProtect(); + else throw std::runtime_error("unknown guest arena test"); + std::printf("guest arena %s passed\n", argv[1]); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +} diff --git a/core/libs/tests/guest_arena/CMakeLists.txt b/core/libs/tests/guest_arena/CMakeLists.txt new file mode 100644 index 0000000000..f84f4e6842 --- /dev/null +++ b/core/libs/tests/guest_arena/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.20) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(GuestArenaTests LANGUAGES CXX) + enable_testing() + set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +endif() + +find_package(Threads REQUIRED) +add_executable(guest_arena_tests ../GuestArenaHost.cpp ../../prx/libc/src/GuestArena.cpp ../../prx/libc/src/GuestWriteWatch.cpp) +target_include_directories(guest_arena_tests PRIVATE ../..) +target_compile_features(guest_arena_tests PRIVATE cxx_std_20) +target_link_libraries(guest_arena_tests PRIVATE Threads::Threads) +set_target_properties(guest_arena_tests PROPERTIES + CXX_EXTENSIONS OFF + RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") + +foreach(mode available unmappable host commit) + add_test(NAME guest_arena_${mode} COMMAND guest_arena_tests ${mode}) + set_tests_properties(guest_arena_${mode} PROPERTIES TIMEOUT 30) +endforeach() From 913adca3aabedfe6bbaabe6cb528225911e636eb Mon Sep 17 00:00:00 2001 From: yripper Date: Tue, 13 Oct 2026 02:20:58 -0300 Subject: [PATCH 034/131] docs(libc): record the macOS guest arena holes as debt Move the explanatory comments into docs/dev/debt/libc.md, as the conventions require. --- core/libs/prx/libc/src/GuestArena.cpp | 3 --- core/libs/tests/GuestArenaHost.cpp | 3 --- docs/dev/debt/libc.md | 1 + 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 008ad0da76..876cf75826 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -210,8 +210,6 @@ private: return EINVAL; } #elif defined(__APPLE__) - // macOS has no /proc/self/maps; dyld, the Rosetta runtime and ranges the kernel never lets a process - // map (max protection none, e.g. 0x10_0000_0000-0x70_0000_0000) all sit inside the PS5 application map. static std::vector> HostMappings() { std::vector> mappings; mach_vm_address_t address = ArenaStart; @@ -229,7 +227,6 @@ private: return mappings; } - // MAP_FIXED_NOREPLACE does not exist on macOS; a fixed mach_vm_map fails instead of replacing. static int ReserveNoReplace(std::uintptr_t address, std::size_t bytes) { mach_vm_address_t wanted = address; const kern_return_t result = mach_vm_map(mach_task_self(), &wanted, bytes, 0, VM_FLAGS_FIXED, MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_NONE, VM_PROT_ALL, VM_INHERIT_DEFAULT); diff --git a/core/libs/tests/GuestArenaHost.cpp b/core/libs/tests/GuestArenaHost.cpp index 6378a14180..6a9a907466 100644 --- a/core/libs/tests/GuestArenaHost.cpp +++ b/core/libs/tests/GuestArenaHost.cpp @@ -51,8 +51,6 @@ std::vector RegionsInArenaRange() { return regions; } -// The kernel keeps ranges inside the PS5 application map that no process may map (max protection none); -// the arena has to come up around them instead of giving up. void CheckAvailable() { Require(GuestArenaAvailable_nid_postfix(), "the guest arena is unavailable on macOS"); std::uintptr_t base = 0; @@ -77,7 +75,6 @@ void CheckSkipsUnmappable() { void CheckHostRegionsKept() { std::size_t hostRegions = 0; for (const auto& region : RegionsInArenaRange()) { - // Reservations made by the arena itself are no-access with full max protection. if (region.maxProtection == VM_PROT_ALL) continue; ++hostRegions; bool rejected = false; diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index e92d357f7f..dca57c44c6 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -48,6 +48,7 @@ - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (file reads and socket receives); a host call that writes guest memory without it (`clock_gettime` and the like into a watched page) fails; - code that changes the protection of watched pages, or maps over them, has to call `GuestWriteWatchProtectionChanged`, and code that asks the kernel for a page's protection has to ask `GuestWriteWatchProtection` first, since a watched page is read-only between a collection and the next write. Nothing calls either yet: the macOS memory paths are not in the tree; - the handler is installed when libc loads and passes other faults to the handler installed before it. A handler installed later has to do the same. +- On macOS the [guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) finds host regions with `mach_vm_region` and reserves the rest with a fixed `mach_vm_map`, which fails instead of replacing, as macOS has no `/proc/self/maps` or `MAP_FIXED_NOREPLACE`. The kernel keeps 0x10_0000_0000-0x70_0000_0000 unmappable for every process (max protection none; macOS 26.5, M1 Pro, arm64 and x86_64 under Rosetta), and dyld and the Rosetta runtime sit near 0x2_0200_0000 and 0x3_0400_0000, so a guest mapping fixed inside those ranges fails where the console places it; mappings without a fixed address are placed around them. - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way - [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated From d42f4963e30eb53f5efb56f9fc743d3da86f79ca Mon Sep 17 00:00:00 2001 From: fdfawef324 Date: Wed, 14 Oct 2026 17:55:39 -0400 Subject: [PATCH 035/131] fix(agc): print the VGT_TF_PARAM and VGT_LS_HS_CONFIG fields of a refused tessellation draw A tessellation draw whose domain, partitioning or output topology the reference path does not support was refused with only VGT_SHADER_STAGES_EN, so a log did not show which configuration was refused. The refusal now prints VGT_TF_PARAM with TYPE, PARTITIONING and TOPOLOGY, and VGT_LS_HS_CONFIG with NUM_PATCHES, HS_NUM_INPUT_CP and HS_NUM_OUTPUT_CP. The fields come before the explanation, so the reason that the APS5_CAPTURE_TRACE log cuts at 256 characters still holds all of them. Field and enumerator names are those of Mesa's src/amd/registers/gfx103.json (enums VGT_TESS_TYPE, VGT_TESS_PARTITION and VGT_TESS_TOPOLOGY). A value with no name there is printed as a number only. The accepted configurations are unchanged. The new UnsupportedTessellationFieldTests drives DecodeState into the refusal with power-of-two triangle and quad patches, an isoline domain, line and point output and unnamed TYPE, PARTITIONING and TOPOLOGY values, and checks the decoded fields of both registers, including the order of the input and output control-point counts. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 22 ++++++++++++++++- .../prx/libSceAgcDriver/tests/Graphics.cpp | 24 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 44cf86c915..92b1fe1805 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -16,6 +16,7 @@ #include #include #include +#include namespace AgcDriver::Graphics { namespace { @@ -49,6 +50,25 @@ std::string zeroMessage(std::uint32_t offset, std::uint32_t value, const char* n return std::string(name) + " is unsupported" + detail; } +std::string tessellationMessage(std::uint32_t config, std::uint32_t parameters, const ShaderRecompiler::TessellationConfiguration& tess) { + static constexpr std::array Types{"TESS_ISOLINE", "TESS_TRIANGLE", "TESS_QUAD"}; + static constexpr std::array Partitionings{"PART_INTEGER", "PART_POW2", "PART_FRAC_ODD", "PART_FRAC_EVEN"}; + static constexpr std::array Topologies{"OUTPUT_POINT", "OUTPUT_LINE", "OUTPUT_TRIANGLE_CW", "OUTPUT_TRIANGLE_CCW"}; + std::ostringstream message; + const auto field = [&](std::string_view name, std::uint32_t value, std::span names) { + message << name << '=' << value; + if (value < names.size()) message << ' ' << names[value]; + }; + message << "VGT_TF_PARAM=0x" << std::hex << parameters << std::dec << " ("; + field("TYPE", tess.domain, Types); + message << ", "; + field("PARTITIONING", tess.partitioning, Partitionings); + message << ", "; + field("TOPOLOGY", tess.outputTopology, Topologies); + message << "), VGT_LS_HS_CONFIG=0x" << std::hex << config << std::dec << " (NUM_PATCHES=" << tess.patchesPerGroup << ", HS_NUM_INPUT_CP=" << tess.inputControlPoints << ", HS_NUM_OUTPUT_CP=" << tess.outputControlPoints << "): only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"; + return message.str(); +} + void zero(const Registers& registers, std::uint32_t offset, std::uint32_t mask, const char* name, RegisterBank bank = RegisterBank::Context) { const auto value = read(registers, offset, bank); if ((value & mask) != 0) throw std::runtime_error(zeroMessage(offset, value, name)); @@ -677,7 +697,7 @@ ShaderStages DecodeShaderStages(const QueueState& queue, bool vertexWorkgroup) { validate(tess.inputControlPoints != 0 && tess.inputControlPoints <= 32 && tess.outputControlPoints != 0 && tess.outputControlPoints <= 32, "invalid tessellation control-point counts"); validate(tess.patchesPerGroup != 0, "VGT_LS_HS_CONFIG.NUM_PATCHES is zero"); if ((parameters & ~0x000600ffu) != 0) Require(false, zeroMessage(0x2db, parameters, "VGT_TF_PARAM donut, factor detection, memory type or reserved state")); - validate((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && (tess.outputTopology == 2 || tess.outputTopology == 3), "only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"); + if (!((tess.domain == 1 || tess.domain == 2) && (tess.partitioning == 0 || tess.partitioning == 2 || tess.partitioning == 3) && (tess.outputTopology == 2 || tess.outputTopology == 3))) validate(false, tessellationMessage(config, parameters, tess).c_str()); result.minTessellationLevel = readFloat(queue.context, 0x287); result.maxTessellationLevel = readFloat(queue.context, 0x286); Require(result.minTessellationLevel >= 0.0f && result.minTessellationLevel <= 63.0f && result.minTessellationLevel <= result.maxTessellationLevel && result.maxTessellationLevel >= 1.0f, "tessellation level clamps need 0 <= VGT_HOS_MIN_TESS_LEVEL <= 63, VGT_HOS_MAX_TESS_LEVEL >= 1 and the minimum at most the maximum"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 3f4bace986..fd76a87698 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -834,6 +834,29 @@ void ShaderStageTests() { expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "missing register"); } +void UnsupportedTessellationFieldTests() { + auto queue = makeState(); + queue.context[0x2d5] = 0x0200210d; + queue.userConfig[0x242] = 9; + queue.userConfig[0x25b] = 0x1fe55; + queue.context[0x286] = 0x42800000; + queue.context[0x287] = 0x3f800000; + for (const auto [config, parameters, fields] : { + std::tuple{0xc355u, 0x40045u, "VGT_TF_PARAM=0x40045 (TYPE=1 TESS_TRIANGLE, PARTITIONING=1 PART_POW2, TOPOLOGY=2 OUTPUT_TRIANGLE_CW), VGT_LS_HS_CONFIG=0xc355 (NUM_PATCHES=85, HS_NUM_INPUT_CP=3, HS_NUM_OUTPUT_CP=3)"}, + std::tuple{0x1033fu, 0x40066u, "VGT_TF_PARAM=0x40066 (TYPE=2 TESS_QUAD, PARTITIONING=1 PART_POW2, TOPOLOGY=3 OUTPUT_TRIANGLE_CCW), VGT_LS_HS_CONFIG=0x1033f (NUM_PATCHES=63, HS_NUM_INPUT_CP=3, HS_NUM_OUTPUT_CP=4)"}, + std::tuple{0xc355u, 0x40048u, "VGT_TF_PARAM=0x40048 (TYPE=0 TESS_ISOLINE, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=2 OUTPUT_TRIANGLE_CW)"}, + std::tuple{0xc355u, 0x40029u, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=1 OUTPUT_LINE)"}, + std::tuple{0xc355u, 0x4000du, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=3 PART_FRAC_EVEN, TOPOLOGY=0 OUTPUT_POINT)"}, + std::tuple{0xc355u, 0x4004bu, "(TYPE=3, PARTITIONING=2 PART_FRAC_ODD, TOPOLOGY=2 OUTPUT_TRIANGLE_CW)"}, + std::tuple{0xc355u, 0x400f1u, "(TYPE=1 TESS_TRIANGLE, PARTITIONING=4, TOPOLOGY=7)"}}) { + queue.context[0x2d6] = config; + queue.context[0x2db] = parameters; + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "VGT_SHADER_STAGES_EN=0x200210d: VGT_TF_PARAM=0x"); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, fields); + expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "): only triangle and quad domains with integer, fractional-even or fractional-odd partitioning and triangle output are supported by the reference path"); + } +} + void NggVertexWorkgroupTests() { auto queue = makeState(); queue.context[0x2d5] = 0x12010; @@ -6635,6 +6658,7 @@ int main(int argc, char** argv) { framebufferLayerTests(); UnwrittenSampleLocationTests(); ShaderStageTests(); + UnsupportedTessellationFieldTests(); NggVertexWorkgroupTests(); inactiveVertexPointSizeTests(); TuningFieldTests(); From 7918cfc49aca3bd6a2a8a1db4f000849b83110c2 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 23:18:11 +0000 Subject: [PATCH 036/131] fix(libSceAgc): reserve complete atomic GDS packets --- core/libs/prx/libSceAgc/Acb/src/Memory.cpp | 2 +- core/libs/prx/libSceAgc/CMakeLists.txt | 13 ++++++++++ .../prx/libSceAgc/tests/AtomicGdsSize.cpp | 25 +++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 core/libs/prx/libSceAgc/tests/AtomicGdsSize.cpp diff --git a/core/libs/prx/libSceAgc/Acb/src/Memory.cpp b/core/libs/prx/libSceAgc/Acb/src/Memory.cpp index 1a93bdbc22..3fb978fbed 100644 --- a/core/libs/prx/libSceAgc/Acb/src/Memory.cpp +++ b/core/libs/prx/libSceAgc/Acb/src/Memory.cpp @@ -43,7 +43,7 @@ std::uint32_t APS5_VABI sceAgcAcbAtomicMemGetSize() { } std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize() { - return 36; + return 44; } std::uint32_t* APS5_VABI sceAgcAcbPrimeUtcl2(CommandBuffer* buf, const volatile void* address, std::uint32_t sizeInBytes) { diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index 0e8338c3da..351466befa 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -81,6 +81,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(agc_atomic_gds_size_tests EXCLUDE_FROM_ALL tests/AtomicGdsSize.cpp) +target_include_directories(agc_atomic_gds_size_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_atomic_gds_size_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_atomic_gds_size_tests) +if(BUILD_TESTING) + set_target_properties(agc_atomic_gds_size_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_atomic_gds_size COMMAND agc_atomic_gds_size_tests) + if(WIN32) + set_tests_properties(agc_atomic_gds_size PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() configure_windows_unwind(libSceAgc) add_test_executable(agc_platform_tests EXCLUDE_FROM_ALL tests/Platform.cpp) diff --git a/core/libs/prx/libSceAgc/tests/AtomicGdsSize.cpp b/core/libs/prx/libSceAgc/tests/AtomicGdsSize.cpp new file mode 100644 index 0000000000..bbc5416bd3 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/AtomicGdsSize.cpp @@ -0,0 +1,25 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include + +extern "C" std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize(); +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); + +int main() { + constexpr std::uint32_t expectedSize = 11u * sizeof(std::uint32_t); + if (sceAgcAcbAtomicGdsGetSize() != expectedSize) { + std::fprintf(stderr, "ACB atomic GDS size does not match its 11-dword packet\n"); + return 1; + } + std::array words{}; + CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0}; + const auto* packet = sceAgcAcbAtomicGds_0900(&buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + if (packet != words.data() || buffer.cursor_up != words.data() + expectedSize / sizeof(std::uint32_t)) { + std::fprintf(stderr, "ACB atomic GDS builder wrote a different size than its size accessor\n"); + return 1; + } +} From af3bd503cbe7ee6d035dca7d111f7dd6d91d634d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:42:42 -0700 Subject: [PATCH 037/131] refactor(agc): declare the largest device-local heap lookup after the budget helpers Its declaration sat right after ClearCachedTextures, where another open change adds a line too. It now follows SampledBudgetReportDue, with the other memory-budget helpers. --- .../prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index ba7825f464..a8d2c19dbe 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -28,10 +28,10 @@ class Recorder; void FlushCachedTextures(VkDevice device); void ClearCachedTextures(VkDevice device); -std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory); std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes); bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport); +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory); struct TextureCacheUse { std::size_t sampledEntries = 0; std::uint64_t sampledBytes = 0; From 62bbbb5dcc0c117252a56277177864c8776f7228 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:36:48 -0700 Subject: [PATCH 038/131] refactor(agc): count an overlay draw packet after its pending waits land Another open change adds a line at the start of the draw-packet branch in Driver::execute. The overlay's draw count now follows landPendingWaits instead, which counts the same packets. --- .../Execution/src/Driver/Packets/PacketExecution.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 903d447aaf..9340e0401c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -277,8 +277,8 @@ void Driver::execute(const Submission& submission) { } } } else if (drawPacket) { - packetCounters.CountDraw(); landPendingWaits(submission.queue); + packetCounters.CountDraw(); bool drawn = false; timed(&WorkerProfile::drawMs, [&] { static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr; From 4ef2af62ea4d7d77ac9e2b09dfe0e54c26119ebe Mon Sep 17 00:00:00 2001 From: SandstromPL Date: Thu, 15 Oct 2026 00:23:52 +0530 Subject: [PATCH 039/131] fix(agc): rasterize a multisampled depth target without MSAA_ENABLE at the pixel center A draw with PA_SC_AA_CONFIG.MSAA_NUM_SAMPLES 2 (4x) and MSAA_ENABLE clear into a 4x depth/stencil and color pair threw "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage", which stops METAL GEAR SOLID: Peace Walker (PPSA18216) at its first presentation (draw B on #5268). A depth target with the same sample count now gets the same pixel center samples as the color targets, so each depth and stencil sample is tested and written with the depth at the pixel center, as GL allows when multisampling is disabled. DB_EQAA Z interpolation or overrasterization and a sample-mask export with a depth target throw on this path. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/Graphics/src/State.cpp | 4 +- .../prx/libSceAgcDriver/tests/Graphics.cpp | 75 ++++++++++++++++++- docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index ff6252d61b..3d31a0b45d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -806,7 +806,9 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) { zero(cx, 0x292, ScanModeMask & ~1u, "scan conversion mode"); if (result.samples > 1 && (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; - Require(filled && !result.depth && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); + Require(filled && !result.alphaToCoverage, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + if (const auto eqaa = find(cx, 0x201); result.depth && eqaa != cx.end() && (eqaa->second & 0x0f0c0000u) != 0) throw std::runtime_error(zeroMessage(0x201, eqaa->second, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)")); + if (const auto control = find(cx, 0x203); result.depth && control != cx.end() && (control->second & 0x100u) != 0) throw std::runtime_error(zeroMessage(0x203, control->second, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export")); result.customSampleLocations = true; result.sampleLocations.fill(0); result.sampleLocationGrid = false; diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 860cc8fb7a..8c1cc05c7f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2271,6 +2271,78 @@ VKAPI_ATTR VkResult VKAPI_CALL sampleLocationDepthProperties(VkPhysicalDevice, V return depthSampleResult; } +void msaaEnableClearDepthTests() { + using AgcDriver::Graphics::DecodeState; + const auto drawB = [] { + auto queue = makeState(); + queue.context[0x2f8] = 0x0020c002; + queue.context[0x292] = 2; + for (std::uint32_t offset = 0x2fe; offset <= 0x30d; ++offset) queue.context[offset] = (offset - 0x2feu) % 4u == 0 ? 0x622ae6aeu : 0u; + queue.context[0x31c] = 0x8028; + queue.context[0x31d] = 0x12000; + queue.context[0x3b8] = 0x0dc00000u; + queue.context[0x1b3] = 2; + queue.context[0x1b4] = 2; + queue.context[0x000] = 0; + queue.context[0x002] = 0; + queue.context[0x007] = (3u << 16u) | 63u; + queue.context[0x00a] = 0; + queue.context[0x00b] = 0; + queue.context[0x010] = 0xa290098b; + queue.context[0x011] = 0x20000180; + for (const auto offset : {0x012u, 0x014u}) queue.context[offset] = 0x100; + queue.context[0x200] = 0x16; + queue.context[0x201] = 0x00010002; + return queue; + }; + const auto requireCentered = [](const AgcDriver::Graphics::State& state, const std::string& what) { + Require(state.samples == 4 && state.depth && state.depth->samples == 4 && state.customSampleLocations, what + " did not keep the 4x depth target"); + const auto locations = AgcDriver::Graphics::SampleLocations(state); + for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(locations[sample].x == 0.5f && locations[sample].y == 0.5f, what + " did not place every sample at the pixel center"); + }; + auto queue = drawB(); + queue.context[0x200] = 0x711; + queue.context[0x011] = 0x20000181; + for (const auto offset : {0x013u, 0x015u}) queue.context[offset] = 0x200; + queue.context[0x10b] = 0x333; + queue.context[0x10c] = 0x00ff0000; + queue.context[0x10d] = 0; + auto state = DecodeState(queue); + Require(state.colors.size() == 1 && state.colors[0].samples == 4, "PPSA18216's draw B lost its 4x color target"); + Require(state.stencilTest && !state.depthTest && state.depth && state.depth->format == VK_FORMAT_D32_SFLOAT_S8_UINT && state.stencilFront.compareOp == VK_COMPARE_OP_ALWAYS, "PPSA18216's draw B (DB_DEPTH_CONTROL 0x711) lost its 4x stencil plane"); + Require(state.stencilFront.passOp == VK_STENCIL_OP_REPLACE && state.stencilFront.failOp == VK_STENCIL_OP_REPLACE && state.stencilFront.depthFailOp == VK_STENCIL_OP_REPLACE && state.stencilFront.writeMask == 0xffu && state.stencilFront.reference == 0u, "PPSA18216's draw B did not decode as a stencil clear to 0"); + requireCentered(state, "a 4x depth and stencil target rasterized without MSAA_ENABLE (PPSA18216's draw B)"); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck refused PPSA18216's draw B"); + queue.context[0x200] = 0x710; + state = DecodeState(queue); + Require(!state.depth && state.samples == 4 && state.colors.size() == 1, "draw B with DB_DEPTH_CONTROL 0x710 bound a depth target"); + queue.context[0x201] = 0x00050002u; + Require(!DecodeState(queue).depth, "DB_EQAA Z interpolation refused a 4x draw without a depth target"); + queue = drawB(); + state = DecodeState(queue); + Require(state.depthTest && state.colors.size() == 1, "a depth-tested 4x depth and color pair lost a target"); + requireCentered(state, "a depth-tested 4x depth and color pair rasterized without MSAA_ENABLE"); + queue.context[0x8e] = 0; + state = DecodeState(queue); + Require(state.colors.empty(), "a depth-only 4x draw without MSAA_ENABLE decoded a color target"); + requireCentered(state, "a 4x depth target rasterized without MSAA_ENABLE"); + for (const auto eqaa : {0x00050002u, 0x00090002u, 0x01010002u, 0x08010002u}) { + queue = drawB(); + queue.context[0x201] = eqaa; + expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with Z interpolation or overrasterization (DB_EQAA)"); + } + queue = drawB(); + queue.context[0x1c4] = 9; + queue.context[0x203] |= 0x100u; + expectFailure([&] { DecodeState(queue); }, "a multisampled depth target rasterized without MSAA_ENABLE with a sample-mask export"); + queue = drawB(); + queue.context[0x2dc] = 0x0000aa01u; + expectFailure([&] { DecodeState(queue); }, "multisampled targets rasterized without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); + queue = drawB(); + queue.context[0x010] = 0xa2900987; + expectFailure([&] { DecodeState(queue); }, "the depth target's sample count differs from the rasterizer's"); +} + void multisampleTests() { using AgcDriver::Graphics::ColorMetadataPass; using AgcDriver::Graphics::DecodeColorBuffer; @@ -2469,7 +2541,7 @@ void multisampleTests() { const auto aliased = AgcDriver::Graphics::SampleLocations(state); for (std::uint32_t sample = 0; sample < state.samples; ++sample) Require(aliased[sample].x == 0.5f && aliased[sample].y == 0.5f, "a 2x target rasterized without MSAA_ENABLE did not place every sample at the pixel center"); queue.context[0x2dc] = 0x0000aa01u; - expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines, a depth target or alpha-to-coverage"); + expectFailure([&] { DecodeState(queue); }, "without MSAA_ENABLE are unsupported for points, lines or alpha-to-coverage"); queue = multisampled(); queue.context[0x292] = 2; queue.context[0x302] = 0x44cc; @@ -6612,6 +6684,7 @@ int main(int argc, char** argv) { cmaskTests(); uint16ExportTests(); uint8x4TargetTests(); + msaaEnableClearDepthTests(); multisampleTests(); MsaaEnableSingleSampleTests(); clipDistanceTests(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ef52ccdb68..d18dff3dab 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -9,7 +9,7 @@ - [GS_FAST_LAUNCH geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (AGC driver) - `VGT_SHADER_STAGES_EN.GS_FAST_LAUNCH = 1` with a point list and `GE_CNTL` groups of one vertex and one primitive decodes as one point per subgroup, with the primitive limit taken from `GE_NGG_SUBGRP_CNTL.PRIM_AMP_FACTOR` and the GS/ES VGPR counts not required. Follows how RADV drives mesh shaders on gfx10.3 (`ac_nir_lower_ngg.c`, the workgroup index comes from the vertex id of the first thread), not measured on the console. Fast launch 2, other group sizes or input primitives and indexed fast-launch draws still throw. PPSA03671's fast-launch draws decode, but the title stops earlier in a wave64 vertex program, so they have not reached the GPU yet. - [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. - [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 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. Other locations of sample 0, absent location registers, points, lines, rectangles, 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. Points, lines, a depth target and alpha-to-coverage throw. SONIC SUPERSTARS (PPSA06888) draws such a 2x target. +- [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 - [User data registers](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw From 28c123b38696306e9389b9320a0516496ce764d8 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Thu, 15 Oct 2026 04:48:47 +0200 Subject: [PATCH 040/131] perf(agc): pipeline queue-0 draw commits behind the packet worker (APS5_PIPELINED_DRAWS) With APS5_PIPELINED_DRAWS=1, queue 0's worker decodes, captures and recompiles a draw and hands it to a committer thread that records it (Graphics::Draw under the GPU lock), so the two run in parallel instead of one after the other. Work the committer has not reached yet is tracked by the guest ranges it will write; a later packet that reads or writes them, or one the pipeline cannot order, drains it. Ordered as commit items instead of drains: - labels (WRITE_DATA, RELEASE_MEM) and same-queue waits on them; APS5_PIPELINE_DRAIN_LABELS=1 drains; - the end of a queue-0 submission and a released REWIND tail; APS5_PIPELINE_DRAIN_SUBMISSIONS=1 drains; - the title's buffer fill kernels; APS5_PIPELINE_DRAIN_FILLS=1 drains; - DMA_DATA immediates and copies (APS5_PIPELINE_DMA=1). A draw's write ranges include the storage images it writes and its exact color and depth target ranges (APS5_EXACT_DRAW_WRITES=1), not estimates. Draw-path costs the pipeline exposed: - ShaderMemory moves, so a commit owns its capture's pages; - a submission made before the worker's current collect epoch began keeps the epoch, so a worker behind a queue of submissions walks a surface once per catch-up (APS5_SUBMISSION_EPOCH_EACH=1 bumps at every submission); a thread remembers the ranges it walked for the whole epoch; - the pipeline signals only threads that wait; the pending-image and unit-shadow registries take a spinning slim lock instead of std::mutex (winpthreads contention was kernel events); ScanPending skips the registry walk when no pending image can overlap; - fewer heap allocations per recorded draw (reused descriptor sets and scratch vectors, buffer pool size classes kept). GTA V Performance mode, prologue bank interior, RTX 5080: 11.7 -> about 20 fps together with the cheaper captures below it. Off by default here; the follow-up turns it on. Co-Authored-By: Claude Opus 5.5 (1M context) Co-Authored-By: Gjon Gojcevic <7807870+gjonn@users.noreply.github.com> --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 1 + .../include/Driver/Draw/DrawPipeline.hpp | 74 +++++++ .../Execution/include/Driver/Driver.hpp | 16 ++ .../include/Driver/Memory/DrawWriteRanges.hpp | 18 ++ .../src/Driver/Dispatch/Dispatch.cpp | 2 + .../Execution/src/Driver/Draw/Draw.cpp | 44 +++++ .../src/Driver/Draw/DrawPipeline.cpp | 185 ++++++++++++++++++ .../src/Driver/Memory/BufferFill.cpp | 32 ++- .../src/Driver/Memory/WriteEvidence.cpp | 5 + .../src/Driver/Memory/WrittenBuffers.cpp | 82 +++++++- .../src/Driver/Packets/PacketExecution.cpp | 81 +++++++- .../src/Driver/Packets/PacketFlush.cpp | 6 + .../src/Driver/Packets/PacketMemory.cpp | 138 +++++++++++++ .../src/Driver/Queues/QueueWorker.cpp | 57 +++++- .../src/Driver/Queues/Submission.cpp | 48 ++++- .../Driver/Synchronization/WaitStatistics.cpp | 8 + .../Execution/src/GuestMemory.cpp | 35 +++- .../Graphics/include/ShaderResources.hpp | 5 + .../Graphics/include/SlimMutex.hpp | 42 ++++ .../Graphics/src/BufferPool.cpp | 4 +- .../Graphics/src/ShaderResources.cpp | 48 ++++- .../libSceAgcDriver/Graphics/src/Texture.cpp | 11 +- .../Graphics/src/UnitShadow.cpp | 3 +- .../tests/execution/DrawWriteRanges.cpp | 116 +++++++++++ .../tests/execution/PipelinedDma.cpp | 68 +++++++ 25 files changed, 1098 insertions(+), 31 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp create mode 100644 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp create mode 100644 core/libs/prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 25e74dc606..10f75f762e 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -150,6 +150,7 @@ set(agcDriverSources ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawCapture.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawDecode.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawLookup.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawPipeline.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawPrecheck.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawRegisterKey.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawTiming.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp new file mode 100644 index 0000000000..8f290e4caa --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp @@ -0,0 +1,74 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAW_DRAWPIPELINE_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAW_DRAWPIPELINE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +class DrawPipeline { +public: + using Commit = std::function; + using Range = std::pair; + enum class DrainReason : std::uint8_t { Packet, Flush, Labels, Capture, Indirect, Submission, Count }; + + static DrawPipeline& Queue0(); + static std::size_t Depth(); + static bool& Active(); + static std::atomic& EpochToken(); + static void FollowEpoch(std::uint64_t token); + + void Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress = 0, std::vector labelBytes = {}); + void Drain(DrainReason reason, std::uint32_t opcode = 0x100); + bool Busy() const { return outstanding.load(std::memory_order_acquire) != 0; } + bool Overlaps(std::uint64_t address, std::size_t bytes); + std::optional PendingLabel(std::uint64_t address, std::size_t bytes); + +private: + struct Item { + Commit commit; + std::vector writes; + std::uint64_t labelAddress = 0; + std::vector labelBytes; + }; + DrawPipeline() = default; + void run(); + void rethrowFailure(); + void report(std::chrono::steady_clock::time_point now); + + std::mutex mutex; + std::condition_variable wake; + std::condition_variable idle; + std::deque items; + std::exception_ptr failure; + std::atomic outstanding{0}; + std::uint32_t idleWaiters = 0; + bool committerWaiting = false; + std::thread thread; + std::uint64_t commits = 0; + std::uint64_t commitNs = 0; + std::uint64_t commitErrors = 0; + std::uint64_t enqueued = 0; + std::uint64_t fullWaitNs = 0; + std::array(DrainReason::Count)> drains{}; + std::array(DrainReason::Count)> drainWaitNs{}; + std::array drainOpcodes{}; + std::uint64_t depthSum = 0; + std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now(); +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 7da8e8022b..f01ee64f43 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -97,6 +97,11 @@ private: void enqueue(Submission submission); void noteHeldAtSubmit(Submission& submission, std::size_t cursor); static void forgetUnfinishedWrites(QueueWorker& worker, const Submission& submission); + static void forgetUnfinishedWrites(QueueWorker& worker, std::span labelWrites); + static bool& completionDeferred(); + void deferCompletion(const Submission& submission); + void submitOpenWork(); + void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span labelWrites); static bool waitFree(const Submission& submission); bool queue0Before(std::uint64_t received) const; bool orderReleased(std::uint32_t queue, std::uint64_t received) const; @@ -207,6 +212,13 @@ private: static void appendWrittenRanges(const ShaderRecompiler::RecompileResult& compiled, std::vector>& ranges); void noteForeignWriter(std::uint64_t begin, std::uint64_t end, std::uint32_t queue); void noteDrawWriters(std::span stages, std::uint32_t queue); + static std::vector> drawWriteRanges(const Graphics::State& graphics, std::span stages); + void commitDraw(std::shared_ptr localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span stages, std::span snapshots, const std::shared_ptr& recipe, const std::vector>& recipeStages, std::uint64_t drawKey); + bool enqueueLabelPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue); + bool enqueueDmaPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state); + void commitDmaStore(std::uint32_t queue, std::uint64_t address, std::span bytes); + void commitLabel(std::uint32_t queue, std::uint64_t address, std::span bytes, bool endOfPipeInterrupt); + void submitDueAfterCommit(VulkanDevice* localDevice); std::optional newestWriterLocked(std::uint64_t begin, std::uint64_t end) const; std::optional newestWriter(std::uint64_t begin, std::uint64_t end); std::string describeWriters(std::uint64_t begin, std::uint64_t end); @@ -264,6 +276,9 @@ private: static Graphics::Recorder::LateStatistics& lateCountsSeen(); static EpochBumps& epochBumps(); static void bumpEpoch(std::uint64_t EpochBumps::*counter); + static std::chrono::steady_clock::time_point& lastEpochBump(); + static std::deque& releasedTails(); + void noteReleasedTails(const Submission& submission); static bool packetEpoch(); static bool labelTryEachPacket(); static std::chrono::microseconds labelFlushDeadline(); @@ -372,6 +387,7 @@ private: std::uint32_t idleWaiters = 0; std::uint64_t queue0Executing = 0; + std::deque queue0Uncommitted; std::atomic orderHolders{0}; std::atomic runningWorkers{0}; std::atomic idleEpoch{0}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp new file mode 100644 index 0000000000..b5e1b03310 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp @@ -0,0 +1,18 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWWRITERANGES_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWWRITERANGES_HPP + +#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp" +#include "prx/libSceAgcDriver/Graphics/include/State.hpp" +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +std::vector> DrawWriteRanges(const Graphics::State& graphics, std::span stages, bool exact); +bool ExactDrawWrites(); + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index c00249bc0c..7002f98de9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp" @@ -87,6 +88,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, pendingDispatchPhases().outcome = DispatchOutcome::FillHle; return; } + if (DrawPipeline::Active()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15); if (!partialLastGroup && indirectArguments == 0 && copyBuffer(queue, submission.queue, packet, std::span(snapshot.code).subspan(codeOffset), userData, compute, localDevice, address)) { pendingDispatchPhases().outcome = DispatchOutcome::CopyHle; return; 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 881c040c61..1adbd297bf 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp @@ -1,5 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" @@ -30,6 +32,8 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe return DrawVerdict::Drawn; }; auto drawParameters = Pm4::ResolveDraw(packet, queue); + const bool pipelined = DrawPipeline::Active(); + if (pipelined && drawParameters.indirect) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Indirect); bool traceIndirect = false; if (const auto verdict = precheckDraw(queue, submission, packet, drawParameters, rejected, traceIndirect)) return *verdict; phaseTiming.Phase(DrawRowPrecheck); @@ -355,6 +359,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe for (const auto& read : reads) memory.push_back({read.address, std::as_bytes(std::span(read.bytes))}); } + if (pipelined && !deferredLabels().labels.empty()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Labels); if (recordQueuedLabelsAfterCapture(submission.queue, memory)) return draw(queue, packet, submission, rejected); bool rectListBuilt = false; @@ -503,6 +508,23 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe return DrawVerdict::Rejected; } } + if (pipelined && !drawParameters.indirect && deferredLabels().labels.empty()) { + phaseTiming.Phase(DrawRowVectors); + auto writes = drawWriteRanges(graphics, stages); + const auto commitQueue = submission.queue; + const auto commitPacket = (packet[0] >> 8u) & 0xffu; + const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed); + auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry)] { + DrawPipeline::FollowEpoch(epoch); + GuestMemory::SetCurrentPacket(commitPacket, commitQueue); + commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey); + }; + auto held = std::make_shared(std::move(commit)); + DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes)); + phaseTiming.Phase(DrawRowLockWait); + timing.Mark("draw_enqueued"); + return drawn(); + } lockForDraw(); noteDrawWriters(stages, submission.queue); phaseTiming.Phase(DrawRowVectors); @@ -523,4 +545,26 @@ DrawVerdict Driver::draw(QueueState& queue, std::span packe return drawn(); } +void Driver::commitDraw(std::shared_ptr localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span stages, std::span snapshots, const std::shared_ptr& recipe, const std::vector>& recipeStages, std::uint64_t drawKey) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (auto current = device.Load(); current != nullptr && current != localDevice) localDevice = std::move(current); + struct SubmitDue { + Driver& driver; + const std::shared_ptr& device; + ~SubmitDue() noexcept(false) { + if (std::uncaught_exceptions() == 0) driver.submitDueAfterCommit(device.get()); + } + } submitDue{*this, localDevice}; + recordLabelsForPacket(localDevice.get(), queue); + noteDrawWriters(stages, queue); + if (recipe != nullptr) { + if (localDevice->DrawFromRecipe(graphics, drawParameters, stages, snapshots, recipe) == RecipeOutcome::Recorded) return; + VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, VulkanDevice::RecipeKind::Draw); + } + std::shared_ptr built; + localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built); + if (built != nullptr) attachDrawRecipe(drawKey, recipeStages, std::move(built)); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp new file mode 100644 index 0000000000..049b061f9d --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp @@ -0,0 +1,185 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include +#include +#include +#include +#include +#include + +namespace AgcDriver::DriverDetail { + +namespace { + +constexpr std::array(DrawPipeline::DrainReason::Count)> DrainNames{"packet", "flush", "labels", "capture", "indirect", "submission"}; + +std::uint64_t elapsedNs(std::chrono::steady_clock::time_point since) { + return static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - since).count()); +} + +} + +DrawPipeline& DrawPipeline::Queue0() { + static DrawPipeline* pipeline = new DrawPipeline(); + return *pipeline; +} + +std::size_t DrawPipeline::Depth() { + static const std::size_t depth = [] { + const char* text = std::getenv("APS5_PIPELINED_DRAWS"); + if (text == nullptr || std::getenv("APS5_LOCKED_DRAW_PREPARE") != nullptr || std::getenv("APS5_DRAW_DRAIN") != nullptr || std::getenv("APS5_DRAIN_ALL") != nullptr || std::getenv("APS5_NO_WORDWISE_CAPTURE") != nullptr) return std::size_t{0}; + const auto value = std::strtoull(text, nullptr, 10); + return value == 0 ? std::size_t{0} : value == 1 ? std::size_t{8} : static_cast(std::min(value, 256)); + }(); + return depth; +} + +bool& DrawPipeline::Active() { + static thread_local bool active = false; + return active; +} + +std::atomic& DrawPipeline::EpochToken() { + static std::atomic token{1}; + return token; +} + +void DrawPipeline::FollowEpoch(std::uint64_t token) { + static thread_local std::uint64_t followed = 0; + if (followed == token) return; + followed = token; + GuestMemory::BumpCollectEpoch(); +} + +void DrawPipeline::rethrowFailure() { + if (failure == nullptr) return; + auto error = std::exchange(failure, nullptr); + std::rethrow_exception(error); +} + +void DrawPipeline::Enqueue(Commit commit, std::vector writes, std::uint64_t labelAddress, std::vector labelBytes) { + static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr; + std::unique_lock lock(mutex); + rethrowFailure(); + if (items.size() >= Depth()) { + const auto start = std::chrono::steady_clock::now(); + ++idleWaiters; + idle.wait(lock, [&] { return items.size() < Depth(); }); + --idleWaiters; + fullWaitNs += elapsedNs(start); + rethrowFailure(); + } + depthSum += items.size(); + ++enqueued; + items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes)}); + outstanding.fetch_add(1, std::memory_order_release); + if (!thread.joinable()) thread = std::thread([this] { run(); }); + if (committerWaiting) wake.notify_one(); + if (!profile) return; + const auto now = std::chrono::steady_clock::now(); + if (now - lastReport >= std::chrono::seconds(10)) report(now); +} + +void DrawPipeline::report(std::chrono::steady_clock::time_point now) { + lastReport = now; + std::string drainText; + for (std::size_t i = 0; i < drains.size(); ++i) { + char item[64]; + std::snprintf(item, sizeof(item), " %s %llu (%.1f ms)", DrainNames[i], static_cast(drains[i]), static_cast(drainWaitNs[i]) / 1e6); + drainText += item; + } + drainText += "; packet drains by opcode:"; + for (int shown = 0; shown < 5; ++shown) { + const auto top = std::max_element(drainOpcodes.begin(), drainOpcodes.begin() + 0x100); + if (*top == 0) break; + char item[32]; + std::snprintf(item, sizeof(item), " 0x%02x x%llu", static_cast(top - drainOpcodes.begin()), static_cast(*top)); + drainText += item; + *top = 0; + } + drainOpcodes = {}; + const auto perCommit = commits == 0 ? 0.0 : static_cast(commitNs) / 1e3 / static_cast(commits); + const auto perDraw = enqueued == 0 ? 0.0 : static_cast(fullWaitNs) / 1e3 / static_cast(enqueued); + const auto queued = enqueued == 0 ? 0.0 : static_cast(depthSum) / static_cast(enqueued); + std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room; drains:%s\n", Depth(), static_cast(commits), perCommit, static_cast(commitErrors), static_cast(enqueued), queued, perDraw, drainText.c_str()); + commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = 0; + drains = {}; + drainWaitNs = {}; +} + +void DrawPipeline::run() { + for (;;) { + Commit* commit = nullptr; + { + std::unique_lock lock(mutex); + committerWaiting = true; + wake.wait(lock, [&] { return !items.empty(); }); + committerWaiting = false; + commit = &items.front().commit; + } + const auto start = std::chrono::steady_clock::now(); + std::exception_ptr fatal; + bool failed = false; + try { + (*commit)(); + } catch (const std::exception& error) { + failed = true; + static std::atomic reported{0}; + if (reported.fetch_add(1, std::memory_order_relaxed) < 20) std::fprintf(stderr, "[draw] pipelined commit failed: %.200s\n", error.what()); + } catch (...) { + fatal = std::current_exception(); + } + const auto spent = elapsedNs(start); + std::lock_guard lock(mutex); + items.pop_front(); + ++commits; + commitNs += spent; + if (failed) ++commitErrors; + if (fatal != nullptr && failure == nullptr) failure = fatal; + outstanding.fetch_sub(1, std::memory_order_release); + if (idleWaiters != 0) idle.notify_all(); + } +} + +void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) { + if (!Busy()) return; + if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline drain under the GPU lock (reason " + std::to_string(static_cast(reason)) + ", opcode " + std::to_string(opcode) + ")"); + const auto start = std::chrono::steady_clock::now(); + std::unique_lock lock(mutex); + ++idleWaiters; + idle.wait(lock, [&] { return items.empty(); }); + --idleWaiters; + const auto index = static_cast(reason); + ++drains[index]; + ++drainOpcodes[std::min(opcode, 0x100)]; + drainWaitNs[index] += elapsedNs(start); + rethrowFailure(); +} + +std::optional DrawPipeline::PendingLabel(std::uint64_t address, std::size_t bytes) { + if (bytes == 0 || bytes > 8) return std::nullopt; + const auto end = address + bytes; + std::lock_guard lock(mutex); + for (auto item = items.rbegin(); item != items.rend(); ++item) { + const bool overlaps = std::any_of(item->writes.begin(), item->writes.end(), [&](const Range& range) { return address < range.second && range.first < end; }); + if (!overlaps) continue; + if (item->labelBytes.empty() || address < item->labelAddress || end > item->labelAddress + item->labelBytes.size()) return std::nullopt; + std::uint64_t value = 0; + std::memcpy(&value, item->labelBytes.data() + (address - item->labelAddress), bytes); + return value; + } + return std::nullopt; +} + +bool DrawPipeline::Overlaps(std::uint64_t address, std::size_t bytes) { + const auto end = address + bytes; + std::lock_guard lock(mutex); + for (const auto& item : items) { + for (const auto& [begin, limit] : item.writes) { + if (address < limit && begin < end) return true; + } + } + return false; +} + +} diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp index 283bdbb2e7..a8a9f85ef4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/BufferFill.cpp @@ -3,6 +3,7 @@ #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" @@ -273,16 +274,17 @@ void Driver::storeBufferFill(std::uint32_t queueId, std::uint64_t base, std::siz ++fills; filledBytes += bytes; if (bytes == 0) return; - GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); - std::lock_guard gpuLock(GuestMemory::GpuMutex()); - const auto holdStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; - auto phaseStart = holdStart; - const auto phase = [&](FillPhase which) { - if (!profile) return; - const auto now = std::chrono::steady_clock::now(); - fillPhaseMs[which] += std::chrono::duration(now - phaseStart).count(); - phaseStart = now; - }; + const auto store = [this, queueId, base, bytes, pattern, localDevice, memoryValueFill, noteMetadata] { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Fill); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto holdStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + auto phaseStart = holdStart; + const auto phase = [&](FillPhase which) { + if (!profile) return; + const auto now = std::chrono::steady_clock::now(); + fillPhaseMs[which] += std::chrono::duration(now - phaseStart).count(); + phaseStart = now; + }; recordLabelsForPacket(localDevice.get(), queueId); phase(FillLabels); @@ -380,5 +382,15 @@ void Driver::storeBufferFill(std::uint32_t queueId, std::uint64_t base, std::siz std::fprintf(stderr, "[fill] %llu buffer fills (%.0f MiB), %llu stored by the CPU; hold phases ms (cumulative):%s; pre-store flushes %llu (%llu stored an image); stored on the GPU: uniform %llu (%.0f MiB), pattern %llu (%.0f MiB), chain copies %llu\n", static_cast(fills.load()), filledBytes.load() / 1048576.0, static_cast(cpuFills.load()), line.c_str(), static_cast(flushes), static_cast(flushed), static_cast(uniformFills), uniformBytes / 1048576.0, static_cast(patternFills), patternBytes / 1048576.0, static_cast(chainCopies)); } } + }; + static const bool orderedFills = std::getenv("APS5_PIPELINE_DRAIN_FILLS") == nullptr; + if (orderedFills && queueId == 0 && DrawPipeline::Active()) { + DrawPipeline::Queue0().Enqueue([store, queueId] { + GuestMemory::SetCurrentPacket(0x15, queueId); + store(); + }, {{base, base + bytes}}); + return; + } + store(); } } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp index bf0f118381..fa2ca447d5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WriteEvidence.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Memory/WriteEvidence.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include @@ -204,6 +205,10 @@ ShaderMemory::PendingWrite Driver::classifyPendingWrite(std::uint64_t address, s } ShaderMemory::PendingWrite Driver::queryPendingWrite(std::uint64_t address, std::size_t bytes, std::span known) { + if (DrawPipeline::Active()) { + auto& pipeline = DrawPipeline::Queue0(); + if (pipeline.Busy() && pipeline.Overlaps(address, bytes)) pipeline.Drain(DrawPipeline::DrainReason::Capture); + } std::uint64_t ValidateCounters::*reason = nullptr; PendingView pending; pending.Load(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp index 682b28dca2..d7c56a6446 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -1,10 +1,19 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" #include "Optimization/ShaderStageInputInfo.hpp" +#include +#include namespace AgcDriver::DriverDetail { +namespace { + template -void Driver::forEachWrittenBuffer(const ShaderRecompiler::RecompileResult& compiled, TVisit&& visit) { +void visitWrittenBuffers(const ShaderRecompiler::RecompileResult& compiled, TVisit&& visit) { for (const auto& binding : compiled.bindings) { if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue; for (std::uint32_t element = 0; element < binding.count; ++element) { @@ -20,6 +29,13 @@ void Driver::forEachWrittenBuffer(const ShaderRecompiler::RecompileResult& compi } } +} + +template +void Driver::forEachWrittenBuffer(const ShaderRecompiler::RecompileResult& compiled, TVisit&& visit) { + visitWrittenBuffers(compiled, std::forward(visit)); +} + void Driver::appendWrittenRanges(const ShaderRecompiler::RecompileResult& compiled, std::vector>& ranges) { forEachWrittenBuffer(compiled, [&](std::uint32_t, std::uint64_t begin, std::uint64_t end, bool) { ranges.emplace_back(begin, end); }); } @@ -46,6 +62,70 @@ void Driver::noteDrawWriters(std::span stages, s } } +bool ExactDrawWrites() { + static const bool exact = std::getenv("APS5_EXACT_DRAW_WRITES") != nullptr; + return exact; +} + +std::vector> DrawWriteRanges(const Graphics::State& graphics, std::span stages, bool exact) { + constexpr std::uint64_t Slack = 64 * 1024; + constexpr std::uint64_t UnknownBytes = 64ull * 1024 * 1024; + std::vector> ranges; + const auto add = [&](std::uint64_t begin, std::uint64_t bytes) { + if (begin != 0) ranges.emplace_back(begin, begin + bytes); + }; + for (const auto& stage : stages) { + if (stage.program == nullptr) continue; + visitWrittenBuffers(*stage.program, [&](std::uint32_t, std::uint64_t begin, std::uint64_t end, bool) { ranges.emplace_back(begin, end); }); + for (const auto& binding : stage.program->bindings) { + if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages || binding.kind != ShaderRecompiler::DescriptorKind::StorageImage) continue; + for (std::uint32_t element = 0; element < binding.count; ++element) { + const bool written = element >= binding.imageWritten.size() || binding.imageWritten[element]; + if (!written || binding.guestDescriptor.size() < (static_cast(element) + 1) * 8) continue; + const auto resource = Graphics::DecodeTextureResource(std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8)); + if (resource.baseAddress == 0) continue; + const auto surfaceBytes = Graphics::DescribeSurface(resource).guestBytes; + add(resource.baseAddress, surfaceBytes != 0 ? surfaceBytes : UnknownBytes); + add(resource.dccAddress, surfaceBytes != 0 ? surfaceBytes / 256 + 1 : UnknownBytes); + } + } + } + const auto addColor = [&](const Graphics::ColorTarget& color) { + if (exact && color.bytes != 0) { + const auto slices = static_cast(std::max(color.depth, 1)); + const auto bytes = static_cast(color.bytes) * slices; + add(color.address, bytes); + add(color.dccAddress, (bytes + 255) / 256); + return; + } + const std::uint64_t bytes = color.bytes != 0 ? static_cast(color.bytes) : UnknownBytes; + const auto base = color.surfaceAddress != 0 ? std::min(color.surfaceAddress, color.address) : color.address; + if (base != 0) ranges.emplace_back(base, color.address + 2 * bytes + Slack); + add(color.dccAddress, bytes / 16 + Slack); + }; + if (graphics.hasColorTarget) addColor(graphics.color); + for (const auto& color : graphics.colors) addColor(color); + if (graphics.depth) { + const auto& depth = *graphics.depth; + if (exact && depth.extent.width != 0 && depth.extent.height != 0) { + const bool d16 = depth.format == VK_FORMAT_D16_UNORM || depth.format == VK_FORMAT_D16_UNORM_S8_UINT; + add(depth.address, Graphics::DepthSliceBytes(depth.extent, d16 ? 2u : 4u)); + add(depth.stencilAddress, Graphics::DepthSliceBytes(depth.extent, 1u)); + } else { + const auto align = [](std::uint64_t value) { return (value + 255) & ~255ull; }; + const auto pixels = align(depth.extent.width) * align(depth.extent.height); + const auto bytes = pixels != 0 ? pixels * 8 + Slack : UnknownBytes; + add(depth.address, bytes); + add(depth.stencilAddress, bytes); + } + } + return ranges; +} + +std::vector> Driver::drawWriteRanges(const Graphics::State& graphics, std::span stages) { + return DrawWriteRanges(graphics, stages, ExactDrawWrites()); +} + std::optional Driver::newestWriterLocked(std::uint64_t begin, std::uint64_t end) const { for (auto it = writtenBuffers.rbegin(); it != writtenBuffers.rend(); ++it) { if (begin < it->end && it->begin < end) return *it; 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 8daba37b06..e304f94d4c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "ThreadOwned.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" @@ -15,6 +16,32 @@ namespace AgcDriver::DriverDetail { +namespace { + +bool orderedFills() { + static const bool ordered = std::getenv("APS5_PIPELINE_DRAIN_FILLS") == nullptr; + return ordered; +} + +bool keepsDrawPipeline(std::span packet, std::uint32_t header, std::uint32_t opcode) { + if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; + if (Pm4::DrawOpcode(opcode)) return true; + switch (opcode) { + case 0x11: case 0x12: case 0x13: case 0x26: case 0x2a: case 0x2f: case 0x42: case 0x58: case 0x59: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: return true; + case 0x46: return packet.size() >= 2 && (packet[1] & 0x3fu) != 0x39u; + case 0x15: return orderedFills(); + case 0x63: case 0x64: case 0x9f: + return packet.size() >= 5 && !DrawPipeline::Queue0().Overlaps(packet[1] | (static_cast(packet[2]) << 32u), static_cast(packet[4]) * 8); + case 0x10: { + const auto operation = (header >> 2u) & 0x3fu; + return operation == 0 || operation == 0x09 || operation == 0x0b || operation == 0x0c || operation == 0x1a; + } + default: return false; + } +} + +} + template void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { static thread_local WorkerProfile profile; @@ -41,6 +68,21 @@ void Driver::execute(const Submission& submission) { } ~FlipHold() { Release(); } } flipHold{*this, submission.holdsFlip}; + const bool pipelined = submission.queue == 0 && DrawPipeline::Depth() != 0; + static const bool orderedWaits = std::getenv("APS5_PIPELINE_DRAIN_LABELS") == nullptr; + DrawPipeline::Active() = pipelined; + struct PipelineDrain { + bool pipelined; + ~PipelineDrain() { + if (!pipelined || std::uncaught_exceptions() == 0) return; + try { + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); + } catch (...) { + } + } + } pipelineDrain{pipelined}; + static const bool orderedCompletion = std::getenv("APS5_PIPELINE_DRAIN_SUBMISSIONS") == nullptr && orderedWaits; + if (pipelined && (submission.suspend || !orderedCompletion)) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); if (submission.suspend) { PerformanceContext timingContext(submissionTiming); PerformanceTimer timing("Driver.Suspend"); @@ -108,7 +150,11 @@ void Driver::execute(const Submission& submission) { auto& packetProfile = ShaderRecompiler::ThreadOwned(packetProfileSlot); ++packetProfile.submissions; - bumpEpoch(&EpochBumps::submissions); + static const bool epochEach = std::getenv("APS5_SUBMISSION_EPOCH_EACH") != nullptr; + if (epochEach || submission.enqueuedAt == std::chrono::steady_clock::time_point{} || submission.enqueuedAt > lastEpochBump()) { + if (!epochEach) noteReleasedTails(submission); + bumpEpoch(&EpochBumps::submissions); + } clearResolvedAhead(); gateOpened() = true; struct ClearAhead { @@ -126,13 +172,31 @@ void Driver::execute(const Submission& submission) { if (APS5_ENABLE_TIMING_LOG && submission.queue == 0 && pendingFrameTiming == nullptr) includeTimingSubmission(submission, false); PerformanceContext timingContext(submission.queue == 0 ? pendingFrameTiming.get() : FrameTiming::Async()); PerformanceTimer timing("Driver.Packet"); + const bool predicated = Pm4::Predicated(header) && queue.predication.operation != 0; + if (pipelined && !predicated && header != FlipPacketHeader && header != RenderingWaitPacketHeader && (enqueueLabelPacket(packet, opcode, submission.queue) || enqueueDmaPacket(packet, opcode, submission.queue, queue))) { + traceLabel(packet, submission.queue); + recent.Record(cursor); + cursor = nextCursor; + continue; + } + if (pipelined && !predicated && (opcode == 0x3c || opcode == 0x93) && header != RenderingWaitPacketHeader && orderedWaits && DrawPipeline::Queue0().Busy()) { + const std::uint64_t awaited = packet[2] | (static_cast(packet[3]) << 32u); + if (const auto value = DrawPipeline::Queue0().PendingLabel(awaited, Pm4::WaitAwaitedBytes(packet)); value.has_value() && Pm4::WaitComparesValue(packet, *value)) { + ++waitOutcomes().fromRecorderSameQueue; + gateOpened() = true; + recent.Record(cursor); + cursor = nextCursor; + continue; + } + } + if (pipelined && DrawPipeline::Queue0().Busy() && !keepsDrawPipeline(packet, header, opcode)) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, header == FlipPacketHeader ? 0xffu : opcode); std::shared_lock deviceUse(deviceReplacement, std::defer_lock); if (opcode != 0x3c && opcode != 0x93 && header != RenderingWaitPacketHeader && header != FlipPacketHeader) deviceUse.lock(); timing.Mark("device_lock"); GuestMemory::SetCurrentPacket(header == FlipPacketHeader ? 0xffffu : opcode, submission.queue); CaptureTrace::Log("packet submission=%llu queue=%x offset=%zu header=%08x words=%zu", static_cast(submission.serial), submission.queue, cursor, header, packet.size()); - if (Pm4::Predicated(header) && queue.predication.operation != 0) { + if (predicated) { landPendingWaits(submission.queue); recordQueuedLabelsBeforeRead(submission.queue); if (!Pm4::PredicationPasses(queue)) { @@ -249,6 +313,7 @@ void Driver::execute(const Submission& submission) { currentPacketOffset() = cursor; if (gateOpened()) { gateOpened() = false; + if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); timed(&WorkerProfile::dispatchMs, [&] { resolveGroupAhead(submission, queue, cursor); }); } if (opcode == 0x15) timed(&WorkerProfile::dispatchMs, [&] { dispatch(queue, packet, submission); }); @@ -337,6 +402,18 @@ void Driver::execute(const Submission& submission) { PerformanceContext endContext(submission.queue == 0 ? frameTiming() : FrameTiming::Async()); PerformanceTimer endTiming("Driver.EndSubmission"); static const bool submitAtEnd = std::getenv("APS5_SUBMIT_AT_END") != nullptr; + if (pipelined && orderedCompletion && deferredLabels().labels.empty()) { + if (submission.rewindTail == nullptr) { + deferCompletion(submission); + return; + } + if ((std::atomic_ref(*const_cast(submission.rewindTail - 1)).load(std::memory_order_acquire) & 0x80000000u) != 0) { + DrawPipeline::Queue0().Enqueue([this] { submitOpenWork(); }, {}); + executeRewindTail(submission); + return; + } + } + if (pipelined) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission); if (!deferredLabels().labels.empty() || Graphics::Recorder::PendingLabelSince().has_value() || Graphics::Recorder::RecordedWorkSinceSubmit() != 0) { auto& costs = submissionCosts(submission.queue); if (!submitAtEnd && submission.rewindTail == nullptr && submission.queue == 0 && workerQueued() != nullptr && workerQueued()->load(std::memory_order_acquire) != 0) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp index 33863d1b2c..410a98b3c8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketFlush.cpp @@ -1,5 +1,6 @@ #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/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -50,6 +51,11 @@ void Driver::flushBetweenPackets(std::uint32_t queue, std::uint32_t header, bool return; } if (reap) lastReapTry = now; + if (DrawPipeline::Active() && (submit || record || capped || recordAtLock)) { + static const bool committerSubmits = std::getenv("APS5_PIPELINE_DRAIN_LABELS") == nullptr; + if (committerSubmits && !record && !recordAtLock && DrawPipeline::Queue0().Busy()) return; + DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Flush); + } std::unique_lock gpuLock(GuestMemory::GpuMutex(), std::defer_lock); if (record) GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Label); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 3609990f90..8a5d4da464 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -1,5 +1,6 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" @@ -9,6 +10,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" #include #include #include @@ -265,4 +267,140 @@ void Driver::dumpSampleCounters(std::uint64_t address) { } } +bool Driver::enqueueDmaPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state) { + static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; + if (!enabled || opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false; + const auto source = ((packet[1] >> 29u) & 3u) | ((packet[6] >> 24u) & 4u) | ((packet[6] >> 25u) & 8u); + constexpr std::size_t immediateLimit = std::size_t{16} << 20u; + if (source == 2) { + const auto store = Pm4::ResolveStore(packet, state, immediateLimit); + if (!store.has_value() || store->Bytes().empty() || !GuestMemory::Accessible(reinterpret_cast(store->address), store->Bytes().size(), true)) return false; + const auto address = store->address; + std::vector bytes(store->Bytes().begin(), store->Bytes().end()); + const auto size = bytes.size(); + DrawPipeline::Queue0().Enqueue([this, queue, address, bytes = std::move(bytes)] { + GuestMemory::SetCurrentPacket(0x50, queue); + commitDmaStore(queue, address, bytes); + }, {{address, address + size}}); + return true; + } + const auto copy = Pm4::DecodeMemoryCopy(packet); + if (!copy.has_value() || copy->destination % 4 != 0 || copy->bytes % 4 != 0 || !GuestMemory::Accessible(reinterpret_cast(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast(copy->destination), copy->bytes, true)) return false; + DrawPipeline::Queue0().Enqueue([this, queue, copy = *copy] { + GuestMemory::SetCurrentPacket(0x50, queue); + constexpr std::size_t gpuStoreLimit = 65536; + if (copy.bytes > gpuStoreLimit) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + if (const auto localDevice = device.Load()) { + recordDeferredLabels(localDevice.get(), queue); + const auto outcome = localDevice->CopyBuffer(copy.destination, copy.source, copy.bytes, 0, std::numeric_limits::max(), 0, 0, queue, [](std::span, std::uint64_t) {}); + if (outcome.path == 1 || outcome.path == 3) return; + } + } + std::vector bytes(copy.bytes); + GuestMemory::Read(copy.source, bytes, 1); + commitDmaStore(queue, copy.destination, bytes); + }, {{copy->destination, copy->destination + copy->bytes}}); + return true; +} + +void Driver::commitDmaStore(std::uint32_t queue, std::uint64_t address, std::span bytes) { + constexpr std::size_t gpuStoreLimit = 65536; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Label); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto localDevice = device.Load(); + Graphics::StorageTexture::FlushPending(address, bytes.size(), nullptr, "packet store", Graphics::PublishScope::PartialUnits); + recordDeferredLabels(localDevice.get(), queue); + int reason = 4; + std::uint64_t stamp = 0; + if (bytes.size() <= gpuStoreLimit && localDevice != nullptr) { + stamp = ++eventSerial; + reason = localDevice->WriteLabelOnGpu(address, bytes, stamp, queue); + } + if (reason == 0 || reason == 5 || reason == 6) { + noteLabelStore(address, bytes, stamp, queue); + if (reason == 0) ++storesOnGpu; + else ++storesBehindCompletions; + } else { + if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle(); + GuestMemory::Write(address, bytes, 1); + noteLabelStore(address, bytes, stamp != 0 ? stamp : ++eventSerial, queue); + ++storesOnCpu; + } + Graphics::Recorder::CloseLabelGroup(GuestMemory::TrackerGeneration()); +} + +bool Driver::enqueueLabelPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue) { + static const bool ordered = std::getenv("APS5_PIPELINE_DRAIN_LABELS") == nullptr; + if (!ordered || !DeferLabels() || (opcode != 0x49 && opcode != 0x37)) return false; + const bool endOfPipeInterrupt = opcode == 0x49 && ((packet[2] >> 24u) & 7u) != 0; + const auto label = Pm4::DecodeLabelWrite(packet); + if (!label.has_value()) { + if (opcode != 0x49) return false; + const bool storesNothing = (packet[2] >> 29u) == 0 || (packet[3] | (static_cast(packet[4]) << 32u)) == 0; + if (!storesNothing) return false; + if (!endOfPipeInterrupt) { + ++noOpLabels; + return true; + } + } + std::uint64_t address = 0; + std::vector bytes; + std::vector writes; + if (label.has_value()) { + const auto stored = label->Bytes(); + address = label->address; + bytes.assign(stored.begin(), stored.end()); + writes.emplace_back(address, address + bytes.size()); + } + if (endOfPipeInterrupt) bumpEpoch(&EpochBumps::drains); + auto stored = bytes; + DrawPipeline::Queue0().Enqueue([this, queue, opcode, address, bytes = std::move(bytes), endOfPipeInterrupt] { + GuestMemory::SetCurrentPacket(opcode, queue); + commitLabel(queue, address, bytes, endOfPipeInterrupt); + }, std::move(writes), address, std::move(stored)); + return true; +} + +void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::span bytes, bool endOfPipeInterrupt) { + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Label); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto localDevice = device.Load(); + recordDeferredLabels(localDevice.get(), queue); + const bool workOpen = Graphics::Recorder::RecordedWorkSinceSubmit() != 0; + int reason = localDevice != nullptr ? 0 : 1; + if (!bytes.empty()) { + const auto stamp = ++eventSerial; + reason = localDevice != nullptr ? localDevice->WriteLabelOnGpu(address, bytes, stamp, queue) : 4; + if (reason != 0 && reason != 5 && reason != 6) { + if (reason != 1 && localDevice != nullptr) localDevice->WaitIdle(); + GuestMemory::Write(address, bytes, 4); + } + noteLabelStore(address, bytes, stamp, queue); + Graphics::Recorder::CloseLabelGroup(GuestMemory::TrackerGeneration()); + countLabelOutcome(reason); + ++immediateLabels; + } else { + ++noOpLabels; + } + if (endOfPipeInterrupt) { + bool deferred = false; + if (localDevice != nullptr && (reason == 0 || reason == 5 || reason == 6)) { + deferred = localDevice->AfterRecordedWork([queue] { AgcDriverDeliverEopInterrupt(queue); }, queue == 0); + if (deferred && workOpen) localDevice->SubmitRecorded(queue == 0); + } + if (!deferred) AgcDriverDeliverEopInterrupt(queue); + } + submitDueAfterCommit(localDevice.get()); +} + +void Driver::submitDueAfterCommit(VulkanDevice* localDevice) { + if (localDevice == nullptr) return; + const auto pending = Graphics::Recorder::PendingLabelSince(); + const bool due = pending.has_value() && std::chrono::steady_clock::now() - *pending >= labelFlushDeadline(); + const bool capped = batchCap() != 0 && Graphics::Recorder::RecordedWorkSinceSubmit() >= batchCap(); + if (due || capped) localDevice->SubmitRecorded(true); +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp index 56db0c1f68..309b16849e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/QueueWorker.cpp @@ -1,5 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp" @@ -54,6 +56,55 @@ void Driver::markCompleted(std::uint64_t serial) { } } +bool& Driver::completionDeferred() { + static thread_local bool deferred = false; + return deferred; +} + +void Driver::deferCompletion(const Submission& submission) { + std::uint64_t received = submission.received; + { + std::lock_guard lock(mutex); + if (queue0Executing != 0) received = std::min(received, queue0Executing); + queue0Uncommitted.push_back(received); + } + completionDeferred() = true; + DrawPipeline::Queue0().Enqueue([this, serial = submission.serial, received, labelWrites = submission.labelWrites] { + completeSubmission(serial, received, labelWrites); + }, {}); +} + +void Driver::submitOpenWork() { + if (deferredLabels().labels.empty() && !Graphics::Recorder::PendingLabelSince().has_value() && Graphics::Recorder::RecordedWorkSinceSubmit() == 0) return; + ++submissionCosts(0).endSubmits; + GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::End); + std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto localDevice = device.Load(); + recordDeferredLabels(localDevice.get(), 0); + if (localDevice != nullptr) localDevice->SubmitRecorded(true); +} + +void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, std::span labelWrites) { + struct Complete { + Driver& driver; + std::uint64_t serial, received; + std::span labelWrites; + ~Complete() { + bool notify = true; + { + std::lock_guard lock(driver.mutex); + driver.markCompleted(serial); + driver.forgetUnfinishedWrites(driver.workers.at(0), labelWrites); + const auto found = std::find(driver.queue0Uncommitted.begin(), driver.queue0Uncommitted.end(), received); + if (found != driver.queue0Uncommitted.end()) driver.queue0Uncommitted.erase(found); + notify = driver.idleWaiters != 0 || driver.orderHolders.load(std::memory_order_acquire) != 0; + } + if (notify) driver.changed.notify_all(); + } + } complete{*this, serial, received, labelWrites}; + submitOpenWork(); +} + const std::atomic*& Driver::workerQueued() { static thread_local const std::atomic* queued = nullptr; return queued; @@ -203,8 +254,10 @@ void Driver::run(std::uint32_t id) noexcept { { std::lock_guard lock(mutex); rethrowFailure(); - markCompleted(submission.serial); - forgetUnfinishedWrites(workers.at(id), submission); + if (!std::exchange(completionDeferred(), false)) { + markCompleted(submission.serial); + forgetUnfinishedWrites(workers.at(id), submission); + } if (id == 0) queue0Executing = 0; notify = idleWaiters != 0 || orderHolders.load(std::memory_order_acquire) != 0; 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 3d428973f2..b7cfef3d62 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -7,6 +7,8 @@ #include "prx/libSceAgcDriver/Execution/include/Pm4.hpp" #include "prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp" #include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp" +#include "ThreadOwned.hpp" +#include "prx/libc/include/HostThreadLocal.hpp" #include #include #include @@ -36,6 +38,33 @@ bool EopRelease(std::span words) { } +std::deque& Driver::releasedTails() { + struct ReleasedTailsStorage {}; + return HostThreadLocal, ReleasedTailsStorage>(); +} + +void Driver::noteReleasedTails(const Submission& submission) { + auto& released = releasedTails(); + released.clear(); + thread_local Submission* scratchSlot = nullptr; + auto& scratch = ShaderRecompiler::ThreadOwned(scratchSlot); + const std::uint32_t* tail = submission.rewindTail; + std::size_t words = submission.rewindWords; + for (std::size_t chunk = 0; chunk < 256 && tail != nullptr; ++chunk) { + if ((std::atomic_ref(*const_cast(tail - 1)).load(std::memory_order_acquire) & 0x80000000u) == 0) break; + released.push_back(tail - 1); + scratch.rewindTail = nullptr; + scratch.rewindWords = 0; + try { + copyCommands(scratch, tail, words); + } catch (const std::exception&) { + break; + } + tail = scratch.rewindTail; + words = scratch.rewindWords; + } +} + void Driver::copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words) { submission.commands.clear(); submission.conditionalEnds.clear(); @@ -248,6 +277,14 @@ void Driver::executeRewindTail(const Submission& stalled) { Submission tail{}; tail.queue = stalled.queue; if (APS5_ENABLE_TIMING_LOG) tail.receivedAt = std::chrono::steady_clock::now(); + auto& released = releasedTails(); + if (!released.empty() && released.front() == stalled.rewindTail - 1) { + released.pop_front(); + tail.enqueuedAt = lastEpochBump(); + } else { + released.clear(); + tail.enqueuedAt = std::chrono::steady_clock::now(); + } copyCommands(tail, stalled.rewindTail, stalled.rewindWords); if (APS5_ENABLE_TIMING_LOG) tail.copiedAt = std::chrono::steady_clock::now(); validate(tail, stalled.rewindTail); @@ -315,10 +352,10 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) { submission.serial = accepted + 1; submission.received = ++eventSerial; + const auto now = std::chrono::steady_clock::now(); + submission.enqueuedAt = now; if (capture != nullptr) capture->Take(captured, submission.serial, captureTime(), submission.heldAtSubmit); if (profile) { - const auto now = std::chrono::steady_clock::now(); - submission.enqueuedAt = now; ++costs.submissions; costs.validateNs += static_cast(std::chrono::duration_cast(validated - copied).count()); costs.copyNs += static_cast(std::chrono::duration_cast((copied - start) + (now - validated)).count()); @@ -373,6 +410,7 @@ bool Driver::waitFree(const Submission& submission) { bool Driver::queue0Before(std::uint64_t received) const { if (queue0Executing != 0 && queue0Executing < received) return true; + if (!queue0Uncommitted.empty() && queue0Uncommitted.front() < received) return true; const auto worker = workers.find(0); if (worker == workers.end()) return false; for (const auto& pending : worker->second.pending) { @@ -455,7 +493,11 @@ void Driver::noteHeldAtSubmit(Submission& submission, std::size_t cursor) { } void Driver::forgetUnfinishedWrites(QueueWorker& worker, const Submission& submission) { - for (const auto dword : submission.labelWrites) { + forgetUnfinishedWrites(worker, submission.labelWrites); +} + +void Driver::forgetUnfinishedWrites(QueueWorker& worker, std::span labelWrites) { + for (const auto dword : labelWrites) { const auto found = worker.unfinishedWrites.find(dword); if (found == worker.unfinishedWrites.end()) continue; if (--found->second == 0) worker.unfinishedWrites.erase(found); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp index 99466faac3..28dfef04d2 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitStatistics.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include @@ -24,8 +25,15 @@ EpochBumps& Driver::epochBumps() { return bumps; } +std::chrono::steady_clock::time_point& Driver::lastEpochBump() { + static thread_local std::chrono::steady_clock::time_point at{}; + return at; +} + void Driver::bumpEpoch(std::uint64_t EpochBumps::*counter) { + lastEpochBump() = std::chrono::steady_clock::now(); GuestMemory::BumpCollectEpoch(); + if (DrawPipeline::Active()) DrawPipeline::EpochToken().fetch_add(1, std::memory_order_relaxed); ++(epochBumps().*counter); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index cd907d4a3d..8850453b34 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -1070,6 +1070,26 @@ struct ThreadCollectMemo { }; thread_local ThreadCollectMemo threadCollectMemo; +struct EpochCollectMap { + std::uint64_t epoch = 0; + std::unordered_map ranges; +}; + +EpochCollectMap& epochCollectMap(std::uint64_t epoch) { + thread_local EpochCollectMap* slot = nullptr; + auto& map = ShaderRecompiler::ThreadOwned(slot); + if (map.epoch != epoch) { + map.ranges.clear(); + map.epoch = epoch; + } + return map; +} + +bool epochCollectMapEnabled() { + static const bool enabled = std::getenv("APS5_NO_EPOCH_COLLECT_MAP") == nullptr; + return enabled; +} + bool sharedCollectMemo() { static const bool shared = std::getenv("APS5_SHARED_COLLECT_MEMO") != nullptr; return shared; @@ -1155,6 +1175,13 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi if (useMemo && !sharedCollectMemo()) { // An entry exists only for a completed walk of an in-arena range, so the tracker is // initialized and watched; nothing below the lock needs asking. + if (epochCollectMapEnabled()) { + const auto& map = epochCollectMap(epoch); + if (const auto found = map.ranges.find(first); found != map.ranges.end() && found->second.unwatched == unwatched && stop <= found->second.end) { + collectMemoHits.fetch_add(1, std::memory_order_relaxed); + return tracker.generation.load(std::memory_order_relaxed); + } + } for (const auto& entry : threadCollectMemo.entries) { if (entry.epoch == epoch && entry.unwatched == unwatched && entry.begin <= first && stop <= entry.end) { // The current generation, not the memoized one: blocks stamped since (MarkWritten, an @@ -1193,7 +1220,13 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi if (collectMemoEnabled()) { const auto serial = unwatchSerial.load(std::memory_order_relaxed); if (sharedCollectMemo()) tracker.memo[tracker.nextMemo++ % tracker.memo.size()] = {first, stop, epoch, serial}; - else threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial}; + else { + threadCollectMemo.entries[threadCollectMemo.next++ % threadCollectMemo.entries.size()] = {first, stop, epoch, serial}; + if (epochCollectMapEnabled()) { + auto& entry = epochCollectMap(epoch).ranges[first]; + if (entry.unwatched != serial || entry.end < stop) entry = {first, stop, epoch, serial}; + } + } } return tracker.generation; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 77058182fe..efefda3a7f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -81,6 +81,8 @@ public: }; SetAllocation Allocate(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind = false, std::optional variableCount = std::nullopt); void Free(const SetAllocation& allocation) noexcept; + SetAllocation AllocateRewritten(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind = false, std::optional variableCount = std::nullopt); + void Recycle(VkDescriptorSetLayout layout, const SetAllocation& allocation) noexcept; // APS5_PROFILE_DRAW counters: layouts served from the map / created, sets allocated, pools opened. struct Stats { std::uint64_t layoutHits = 0; @@ -106,6 +108,7 @@ private: std::vector pools; std::vector updateAfterBindPools; std::vector dedicated; + std::unordered_map> recycled; Stats stats; }; @@ -152,6 +155,8 @@ public: std::shared_ptr buffer; }; DescriptorCache* cache = nullptr; + VkDescriptorSetLayout layout = VK_NULL_HANDLE; + VkDescriptorSetLayout fragmentLayout = VK_NULL_HANDLE; DescriptorCache::SetAllocation fragmentAllocation; DescriptorCache::SetAllocation allocation; std::vector snapshots; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp new file mode 100644 index 0000000000..32de92284c --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp @@ -0,0 +1,42 @@ +#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SLIMMUTEX_HPP +#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SLIMMUTEX_HPP + +#include +#include +#if defined(__x86_64__) || defined(_M_X64) +#include +#endif + +namespace AgcDriver::Graphics { + +class SlimMutex { +public: + SlimMutex() = default; + SlimMutex(const SlimMutex&) = delete; + SlimMutex& operator=(const SlimMutex&) = delete; + + void lock() { + for (unsigned spins = 0;;) { + if (!locked.exchange(true, std::memory_order_acquire)) return; + while (locked.load(std::memory_order_relaxed)) { + if (spins < 128) { + ++spins; +#if defined(__x86_64__) || defined(_M_X64) + _mm_pause(); +#endif + } else { + std::this_thread::yield(); + } + } + } + } + bool try_lock() { return !locked.load(std::memory_order_relaxed) && !locked.exchange(true, std::memory_order_acquire); } + void unlock() { locked.store(false, std::memory_order_release); } + +private: + std::atomic locked{false}; +}; + +} + +#endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp index 4ae8a5074f..6daab29916 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp @@ -1,5 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" +#include #include #include #include @@ -236,7 +238,6 @@ std::optional BufferPool::Take(std::size_t bytes, VkBufferUsag } auto result = found->second.back().allocation; found->second.pop_back(); - if (found->second.empty()) tier.free.erase(found); --tier.slots; tier.retainedBytes -= result.allocationBytes; ++tier.hits; @@ -249,7 +250,6 @@ void BufferPool::evictOldest(Tier& tier, std::vector& evicted) evicted.push_back(oldest->second.front().allocation); tier.retainedBytes -= oldest->second.front().allocation.allocationBytes; oldest->second.pop_front(); - if (oldest->second.empty()) tier.free.erase(oldest); --tier.slots; ++tier.evictions; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 8e5b85e634..090c716486 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -3470,6 +3470,34 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l return {set, pool, chain.size() - 1, updateAfterBind}; } +DescriptorCache::SetAllocation DescriptorCache::AllocateRewritten(VkDescriptorSetLayout layout, std::span sizes, bool updateAfterBind, std::optional variableCount) { + { + std::lock_guard lock(mutex); + if (const auto found = recycled.find(layout); found != recycled.end() && !found->second.empty()) { + const auto allocation = found->second.back(); + found->second.pop_back(); + return allocation; + } + } + return Allocate(layout, sizes, updateAfterBind, variableCount); +} + +void DescriptorCache::Recycle(VkDescriptorSetLayout layout, const SetAllocation& allocation) noexcept { + if (allocation.set == VK_NULL_HANDLE || allocation.pool == VK_NULL_HANDLE) return; + { + std::lock_guard lock(mutex); + try { + auto& list = recycled[layout]; + if (list.size() < 4096) { + list.push_back(allocation); + return; + } + } catch (...) { + } + } + Free(allocation); +} + void DescriptorCache::Free(const SetAllocation& allocation) noexcept { if (allocation.set == VK_NULL_HANDLE || allocation.pool == VK_NULL_HANDLE) return; std::lock_guard lock(mutex); @@ -4387,8 +4415,14 @@ bool ShaderResources::LayoutUpdateAfterBind() const { } ShaderResources::DrawBindings::~DrawBindings() { - if (cache != nullptr && allocation.set != VK_NULL_HANDLE) cache->Free(allocation); - if (cache != nullptr && fragmentAllocation.set != VK_NULL_HANDLE) cache->Free(fragmentAllocation); + if (cache == nullptr) return; + const auto release = [&](VkDescriptorSetLayout setLayout, const DescriptorCache::SetAllocation& set) { + if (set.set == VK_NULL_HANDLE) return; + if (setLayout != VK_NULL_HANDLE) cache->Recycle(setLayout, set); + else cache->Free(set); + }; + release(layout, allocation); + release(fragmentLayout, fragmentAllocation); } std::optional> ShaderResources::MovedReadOnlyBuffers(std::span shaders, Recorder& recorder) const { @@ -4451,7 +4485,9 @@ std::shared_ptr ShaderResources::PrepareDrawBindi const auto maxSnapshotBytes = maxDrawSnapshotBytes; std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); - std::vector selected; + thread_local std::vector* selectedSlot = nullptr; + auto& selected = ShaderRecompiler::ThreadOwned(selectedSlot); + selected.clear(); std::array stamp{}; bool stamped = false; for (std::size_t index = 0; index < allocations.size(); ++index) { @@ -4539,11 +4575,13 @@ std::shared_ptr ShaderResources::PrepareDrawBindi const auto variableOf = [&](std::size_t set) { return tableBindings[set] != std::numeric_limits::max() ? std::optional(std::max(1u, static_cast(bindings[tableBindings[set]].imageAllocations.size()))) : std::nullopt; }; result->cache = context.descriptorCache; if (!descriptions[0].empty() || splitSets) { - result->allocation = result->cache->Allocate(_layout, sizesOf(0), updateAfterBind, variableOf(0)); + result->allocation = result->cache->AllocateRewritten(_layout, sizesOf(0), updateAfterBind, variableOf(0)); + result->layout = _layout; Require(result->allocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); } if (!descriptions[1].empty() || splitSets) { - result->fragmentAllocation = result->cache->Allocate(fragment.layout, sizesOf(1), updateAfterBind, variableOf(1)); + result->fragmentAllocation = result->cache->AllocateRewritten(fragment.layout, sizesOf(1), updateAfterBind, variableOf(1)); + result->fragmentLayout = fragment.layout; Require(result->fragmentAllocation.set != VK_NULL_HANDLE, "draw snapshot descriptor allocation failed"); } auto buffers = descriptorBuffers; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index ebd2feb5ad..e999c167a4 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp" #include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp" #include "prx/libSceAgcDriver/Graphics/include/Texture.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp" #include "prx/libc/include/General.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" @@ -1189,7 +1190,7 @@ private: // Storage images whose results have not reached guest memory yet. struct PendingWrites { - std::mutex mutex; + SlimMutex mutex; PendingList textures; // Images taken out of `textures` by a FlushPending still storing them (see adjacentPendingUnchanged). std::vector flushing; @@ -3758,9 +3759,11 @@ bool StorageTexture::ScanPending(std::span queries) { query.found = nullptr; if (query.end <= query.begin) continue; const auto bytes = static_cast(query.end - query.begin); - for (const auto* texture : pending.textures) { - if (query.found == nullptr && texture->descriptor.baseAddress == query.begin && texture->guestBytes >= bytes) query.found = texture; - if (texture != query.except && texture->overlaps(query.begin, bytes)) query.overlaps = true; + if (pending.textures.MayOverlap(query.begin, bytes)) { + for (const auto* texture : pending.textures) { + if (query.found == nullptr && texture->descriptor.baseAddress == query.begin && texture->guestBytes >= bytes) query.found = texture; + if (texture != query.except && texture->overlaps(query.begin, bytes)) query.overlaps = true; + } } for (const auto* texture : pending.flushing) { if (texture != query.except && texture->overlaps(query.begin, bytes)) query.overlaps = true; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp index a6a8c6af1d..45d57d64da 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/UnitShadow.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp" +#include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp" #include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp" #include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/Resources.hpp" @@ -97,7 +98,7 @@ struct UnitShadow { }; struct Shadows { - std::mutex mutex; + SlimMutex mutex; std::map> byBase; std::uint64_t liveBytes = 0; std::uint64_t peakBytes = 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp new file mode 100644 index 0000000000..9b47b48ccc --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp @@ -0,0 +1,116 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "Recompiler.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using Range = std::pair; +using Ranges = std::vector; + +void Require(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("draw write ranges: " + what); +} + +bool Contains(const Ranges& ranges, std::uint64_t begin, std::uint64_t end) { + return std::any_of(ranges.begin(), ranges.end(), [&](const Range& range) { return range.first <= begin && end <= range.second; }); +} + +bool Touches(const Ranges& ranges, std::uint64_t begin, std::uint64_t end) { + return std::any_of(ranges.begin(), ranges.end(), [&](const Range& range) { return range.first < end && begin < range.second; }); +} + +std::array ImageWords(std::uint64_t base, std::uint32_t width, std::uint32_t height) { + std::array words{}; + const auto base40 = base >> 8u; + words[0] = static_cast(base40); + words[1] = static_cast((base40 >> 32u) & 0xffu) | (56u << 20u) | (((width - 1u) & 0x3u) << 30u); + words[2] = ((width - 1u) >> 2u) | ((height - 1u) << 14u); + words[3] = 4u | (5u << 3u) | (6u << 6u) | (7u << 9u) | (9u << 28u); + return words; +} + +} + +int main() { + try { + using AgcDriver::DriverDetail::DrawWriteRanges; + constexpr std::uint64_t written = 0x100000000ull; + constexpr std::uint64_t readOnly = 0x200000000ull; + ShaderRecompiler::RecompileResult program; + ShaderRecompiler::DescriptorBinding images; + images.kind = ShaderRecompiler::DescriptorKind::StorageImage; + images.role = ShaderRecompiler::DescriptorRole::GuestImages; + images.count = 2; + for (const auto base : {written, readOnly}) { + const auto words = ImageWords(base, 256, 128); + images.guestDescriptor.insert(images.guestDescriptor.end(), words.begin(), words.end()); + } + images.imageWritten = {true, false}; + program.bindings.push_back(images); + const std::array stages{{{ShaderRecompiler::ShaderStage::Fragment, &program, 0}}}; + AgcDriver::Graphics::State state{}; + const auto resource = AgcDriver::Graphics::DecodeTextureResource(std::span(images.guestDescriptor).subspan(0, 8)); + const auto surfaceBytes = AgcDriver::Graphics::DescribeSurface(resource).guestBytes; + Require(resource.baseAddress == written && surfaceBytes >= 256u * 128u * 4u, "the test T# does not decode as meant"); + for (const bool exact : {false, true}) { + const auto ranges = DrawWriteRanges(state, stages, exact); + Require(Contains(ranges, written, written + surfaceBytes), "a storage image the draw stores to is not a write range"); + Require(!Touches(ranges, readOnly, readOnly + surfaceBytes), "a storage image the draw only reads is a write range"); + } + program.bindings[0].imageWritten.clear(); + Require(Contains(DrawWriteRanges(state, stages, true), readOnly, readOnly + surfaceBytes), "an image without written flags is not a write range"); + AgcDriver::Graphics::State targets{}; + targets.hasColorTarget = true; + auto& color = targets.color; + color.address = 0x300000000ull; + color.surfaceAddress = color.address; + color.extent = {1920, 1080}; + color.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget; + color.elementBytes = 4; + const AgcDriver::Graphics::ColorTargetLayout layout(1920, 1080, color.tileMode, 4); + color.bytes = layout.Bytes(); + color.dccAddress = 0x380000000ull; + AgcDriver::Graphics::DepthTarget depth{}; + depth.address = 0x400000000ull; + depth.stencilAddress = 0x480000000ull; + depth.extent = {1920, 1080}; + depth.format = VK_FORMAT_D32_SFLOAT_S8_UINT; + targets.depth = depth; + const std::span none; + const auto exact = DrawWriteRanges(targets, none, true); + const auto estimated = DrawWriteRanges(targets, none, false); + std::size_t last = 0; + for (std::uint32_t y = 0; y < 1080; y += 7) { + for (std::uint32_t x = 0; x < 1920; x += 5) last = std::max(last, layout.Offset(x, y)); + } + last = std::max(last, layout.Offset(1919, 1079)); + Require(last + 4 <= color.bytes, "the color layout addresses past its bytes"); + Require(Contains(exact, color.address, color.address + color.bytes), "the exact color range misses the layout"); + Require(!Touches(exact, color.address + color.bytes, color.address + color.bytes + 1), "the exact color range runs past the layout"); + Require(Contains(exact, color.dccAddress, color.dccAddress + color.bytes / 256) && !Touches(exact, color.dccAddress + (color.bytes + 255) / 256, color.dccAddress + (color.bytes + 255) / 256 + 1), "the exact DCC range is not one key byte per 256 surface bytes"); + const auto depthBytes = AgcDriver::Graphics::DepthSliceBytes(depth.extent, 4); + const auto stencilBytes = AgcDriver::Graphics::DepthSliceBytes(depth.extent, 1); + Require(Contains(exact, depth.address, depth.address + depthBytes) && !Touches(exact, depth.address + depthBytes, depth.address + depthBytes + 1), "the exact depth range is not DepthSliceBytes"); + Require(Contains(exact, depth.stencilAddress, depth.stencilAddress + stencilBytes) && !Touches(exact, depth.stencilAddress + stencilBytes, depth.stencilAddress + stencilBytes + 1), "the exact stencil range is not DepthSliceBytes"); + for (const auto& [begin, end] : exact) Require(Contains(estimated, begin, end), "an exact range is not inside the estimates"); + std::printf("draw write ranges: color %zu bytes (estimate %llu), depth %llu, stencil %llu\n", color.bytes, static_cast(2 * color.bytes + 65536), static_cast(depthBytes), static_cast(stencilBytes)); + std::puts("draw write ranges tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp new file mode 100644 index 0000000000..c470b4fdc0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp @@ -0,0 +1,68 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" +#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" +#include "prx/libc/include/Shutdown.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void check(bool condition, const std::string& what) { + if (!condition) throw std::runtime_error("pipelined DMA: " + what); +} + +std::array DmaData(bool immediate, std::uint64_t source, std::uint32_t data, std::uint64_t destination, std::uint32_t bytes) { + return {0xc0055000u, immediate ? (2u << 29u) : 0u, immediate ? data : static_cast(source), immediate ? 0u : static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), bytes}; +} + +void Submit(std::vector& words) { + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); +} + +} + +int main() { +#ifdef _WIN32 + _putenv_s("APS5_PIPELINED_DRAWS", "1"); + _putenv_s("APS5_PIPELINE_DMA", "1"); +#else + setenv("APS5_PIPELINED_DRAWS", "1", 1); + setenv("APS5_PIPELINE_DMA", "1", 1); +#endif + try { + alignas(256) static std::array first{}; + alignas(256) static std::array second{}; + first.fill(0); + second.fill(0); + using namespace AgcDriver::DriverDetail; + const auto drainedBefore = storesDrained.load(); + const auto cpuBefore = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load(); + std::vector words; + const auto fill = DmaData(true, 0, 0x5eed1234u, reinterpret_cast(first.data()), static_cast(sizeof(first))); + const auto copy = DmaData(false, reinterpret_cast(first.data()), 0, reinterpret_cast(second.data()), 512); + words.insert(words.end(), fill.begin(), fill.end()); + words.insert(words.end(), copy.begin(), copy.end()); + Submit(words); + AgcDriverWaitIdle_nid_postfix(); + for (const auto word : first) check(word == 0x5eed1234u, "the immediate fill did not land"); + for (std::size_t i = 0; i < second.size(); ++i) check(second[i] == (i < 128 ? 0x5eed1234u : 0u), "the copy did not see the fill before it, or wrote past its bytes"); + const auto stores = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load() - cpuBefore; + check(stores == 2, "the DMA_DATA packets were not committed (" + std::to_string(stores) + " committed stores)"); + check(storesDrained.load() == drainedBefore, "a DMA_DATA packet still drained"); + LibcRunShutdown_nid_postfix(); + std::puts("pipelined DMA tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { LibcRunShutdown_nid_postfix(); } catch (...) {} + return 1; + } +} From 411097197b165e0d84e03c75df8fc6369718075c Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Tue, 13 Oct 2026 17:36:03 +0200 Subject: [PATCH 041/131] fix(agc): a null storage image in a draw's written set writes nothing DrawWriteRanges decoded every written storage image element, and DecodeTextureResource refuses a zero base address, so a draw with a null storage image bound failed (PPSA01317) instead of skipping it. The element is now skipped by the binder's own null rule (base address words zero) before it is decoded. agc_driver_draw_write_ranges binds a null image next to a written one. Co-Authored-By: Gjon Gojcevic <7807870+gjonn@users.noreply.github.com> Co-Authored-By: SleglJan <90033145+SleglJan@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Execution/src/Driver/Memory/WrittenBuffers.cpp | 4 +++- .../tests/execution/DrawWriteRanges.cpp | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp index d7c56a6446..5da9b3dac8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp @@ -82,7 +82,9 @@ std::vector> DrawWriteRanges(const Graph for (std::uint32_t element = 0; element < binding.count; ++element) { const bool written = element >= binding.imageWritten.size() || binding.imageWritten[element]; if (!written || binding.guestDescriptor.size() < (static_cast(element) + 1) * 8) continue; - const auto resource = Graphics::DecodeTextureResource(std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8)); + const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * 8, 8); + if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue; + const auto resource = Graphics::DecodeTextureResource(words); if (resource.baseAddress == 0) continue; const auto surfaceBytes = Graphics::DescribeSurface(resource).guestBytes; add(resource.baseAddress, surfaceBytes != 0 ? surfaceBytes : UnknownBytes); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp index 9b47b48ccc..56a46eeb12 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DrawWriteRanges.cpp @@ -72,6 +72,16 @@ int main() { } program.bindings[0].imageWritten.clear(); Require(Contains(DrawWriteRanges(state, stages, true), readOnly, readOnly + surfaceBytes), "an image without written flags is not a write range"); + auto nullImage = ImageWords(0, 256, 128); + nullImage[0] = 0; + nullImage[1] &= ~0xffu; + std::copy(nullImage.begin(), nullImage.end(), program.bindings[0].guestDescriptor.begin() + 8); + program.bindings[0].imageWritten = {true, true}; + for (const bool exact : {false, true}) { + const auto ranges = DrawWriteRanges(state, stages, exact); + Require(Contains(ranges, written, written + surfaceBytes), "a written image next to a null one is not a write range"); + Require(!Touches(ranges, 0, surfaceBytes), "a null storage image is a write range"); + } AgcDriver::Graphics::State targets{}; targets.hasColorTarget = true; auto& color = targets.color; From bcba3b5b3ea829533971ab81108953b13e740192 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Tue, 13 Oct 2026 17:43:13 +0200 Subject: [PATCH 042/131] fix(agc): a pipelined release with INT_SEL 3 raises no interrupt The pipelined label path treated every nonzero RELEASE_MEM INT_SEL as an end-of-pipe interrupt; main's PacketMemory excludes 3 (send data after write confirm). agc_driver failed on it with APS5_PIPELINED_DRAWS=1 ("release with INT_SEL send data after write confirm raised an interrupt"). Co-Authored-By: TheAggie <67454+TheAggie@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Execution/src/Driver/Packets/PacketMemory.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 8a5d4da464..f8704cd755 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -334,7 +334,8 @@ void Driver::commitDmaStore(std::uint32_t queue, std::uint64_t address, std::spa bool Driver::enqueueLabelPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue) { static const bool ordered = std::getenv("APS5_PIPELINE_DRAIN_LABELS") == nullptr; if (!ordered || !DeferLabels() || (opcode != 0x49 && opcode != 0x37)) return false; - const bool endOfPipeInterrupt = opcode == 0x49 && ((packet[2] >> 24u) & 7u) != 0; + const auto interruptSelect = opcode == 0x49 ? (packet[2] >> 24u) & 7u : 0u; + const bool endOfPipeInterrupt = interruptSelect != 0 && interruptSelect != 3; const auto label = Pm4::DecodeLabelWrite(packet); if (!label.has_value()) { if (opcode != 0x49) return false; From f2fd6e67d365f54ac3490429dfeb2a7a31d09a2a Mon Sep 17 00:00:00 2001 From: ivica00 Date: Thu, 15 Oct 2026 04:51:21 +0200 Subject: [PATCH 043/131] perf(agc): pipeline DMA_DATA by default and do not drain around it DMA_DATA on queue 0 joins the draw pipeline unless APS5_NO_PIPELINE_DMA is set. A copy that does not overlap uncommitted draws, a SET_PREDICATION, and a NOP other than a dispatch reset leave the queue in place. The source bytes are snapshotted in packet order, an immediate fill still notes depth and color metadata, and a predication that reads an uncommitted range waits for it. Marker push and pop no longer allocate a string per packet. --- .../src/Driver/Packets/PacketExecution.cpp | 14 ++++++++++++-- .../src/Driver/Packets/PacketMemory.cpp | 18 ++++++++++++------ .../prx/libSceAgcDriver/Execution/src/Pm4.cpp | 3 +-- 3 files changed, 25 insertions(+), 10 deletions(-) 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 e304f94d4c..9f63487212 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -27,14 +27,23 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; if (Pm4::DrawOpcode(opcode)) return true; switch (opcode) { - case 0x11: case 0x12: case 0x13: case 0x26: case 0x2a: case 0x2f: case 0x42: case 0x58: case 0x59: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: return true; + case 0x11: case 0x12: case 0x13: case 0x20: case 0x26: case 0x2a: case 0x2f: case 0x42: case 0x58: case 0x59: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: return true; case 0x46: return packet.size() >= 2 && (packet[1] & 0x3fu) != 0x39u; case 0x15: return orderedFills(); case 0x63: case 0x64: case 0x9f: return packet.size() >= 5 && !DrawPipeline::Queue0().Overlaps(packet[1] | (static_cast(packet[2]) << 32u), static_cast(packet[4]) * 8); + case 0x50: { + if (packet.size() < 7) return false; + const auto dst = ((packet[1] >> 20u) & 3u) | ((packet[6] >> 25u) & 4u) | ((packet[6] >> 26u) & 8u); + if (dst == 1 || dst == 2) return true; + const std::size_t bytes = packet[6] & 0x3ffffffu; + if (bytes == 0) return true; + const auto destination = packet[4] | (static_cast(packet[5]) << 32u); + return !DrawPipeline::Queue0().Overlaps(destination, bytes); + } case 0x10: { const auto operation = (header >> 2u) & 0x3fu; - return operation == 0 || operation == 0x09 || operation == 0x0b || operation == 0x0c || operation == 0x1a; + return operation != 0x09; } default: return false; } @@ -197,6 +206,7 @@ void Driver::execute(const Submission& submission) { GuestMemory::SetCurrentPacket(header == FlipPacketHeader ? 0xffffu : opcode, submission.queue); CaptureTrace::Log("packet submission=%llu queue=%x offset=%zu header=%08x words=%zu", static_cast(submission.serial), submission.queue, cursor, header, packet.size()); if (predicated) { + if (pipelined && DrawPipeline::Queue0().Busy() && DrawPipeline::Queue0().Overlaps(queue.predication.address, queue.predication.operation == 3 ? 8 : 4)) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode); landPendingWaits(submission.queue); recordQueuedLabelsBeforeRead(submission.queue); if (!Pm4::PredicationPasses(queue)) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index f8704cd755..8eb13b3957 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -268,14 +268,21 @@ void Driver::dumpSampleCounters(std::uint64_t address) { } bool Driver::enqueueDmaPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state) { - static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; + static const bool enabled = std::getenv("APS5_NO_PIPELINE_DMA") == nullptr; if (!enabled || opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false; - const auto source = ((packet[1] >> 29u) & 3u) | ((packet[6] >> 24u) & 4u) | ((packet[6] >> 25u) & 8u); + const auto source = Pm4::DmaSource(packet); + const std::size_t length = packet[6] & 0x3ffffffu; constexpr std::size_t immediateLimit = std::size_t{16} << 20u; - if (source == 2) { - const auto store = Pm4::ResolveStore(packet, state, immediateLimit); + constexpr std::size_t gpuStoreLimit = 65536; + if (source == 0 || source == 3) Graphics::RequireNoMultisampleTarget(packet[2] | (static_cast(packet[3]) << 32u), length, "a DMA copy"); + if (source == 2 || length <= gpuStoreLimit) { + const auto store = Pm4::ResolveStore(packet, state, source == 2 ? immediateLimit : gpuStoreLimit); if (!store.has_value() || store->Bytes().empty() || !GuestMemory::Accessible(reinterpret_cast(store->address), store->Bytes().size(), true)) return false; const auto address = store->address; + if (source == 2 && Pm4::DmaDestination(packet) != 1) { + Graphics::NoteDepthMetadataFill(address, length, packet[2]); + Graphics::NoteColorMetadataFill(address, length, packet[2]); + } std::vector bytes(store->Bytes().begin(), store->Bytes().end()); const auto size = bytes.size(); DrawPipeline::Queue0().Enqueue([this, queue, address, bytes = std::move(bytes)] { @@ -288,8 +295,7 @@ bool Driver::enqueueDmaPacket(std::span packet, std::uint32 if (!copy.has_value() || copy->destination % 4 != 0 || copy->bytes % 4 != 0 || !GuestMemory::Accessible(reinterpret_cast(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast(copy->destination), copy->bytes, true)) return false; DrawPipeline::Queue0().Enqueue([this, queue, copy = *copy] { GuestMemory::SetCurrentPacket(0x50, queue); - constexpr std::size_t gpuStoreLimit = 65536; - if (copy.bytes > gpuStoreLimit) { + { GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy); std::lock_guard gpuLock(GuestMemory::GpuMutex()); if (const auto localDevice = device.Load()) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index bdee46ee16..edf0d3c9d6 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -986,9 +986,8 @@ void Execute(std::span packet, QueueState& queue) { switch ((packet[0] >> 2u) & 0x3fu) { case 0: return; case 0x09: queue = std::move(*std::make_unique()); return; - case 0x0b: queue.markers.emplace_back(reinterpret_cast(packet.data() + 1)); return; + case 0x0b: case 0x0c: - if (!queue.markers.empty()) queue.markers.pop_back(); return; case 0x1a: if (TraceContextState()) std::fprintf(stderr, "[context] op %u: %zu registers, saved %d, cb0 %x info %x mask %x\n", packet[1], queue.context.size(), queue.savedContext.has_value() ? 1 : 0, queue.context.contains(0x318) ? queue.context.at(0x318) : 0u, queue.context.contains(0x31c) ? queue.context.at(0x31c) : 0u, queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u); From 62022e9362a27432e007cea132ecd9ca0909bb2b Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:38:16 -0700 Subject: [PATCH 044/131] refactor(agc): keep the private heap lookup and expose a public wrapper LargestDeviceLocalHeap now calls ShaderResources.cpp's private largestDeviceLocalHeap instead of replacing it, so the file's callers stay as they are on main and other changes that call the private helper still build with this one. --- .../Graphics/src/ShaderResources.cpp | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 7a97bde178..36e722f06c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -481,6 +481,14 @@ std::shared_ptr sampledStorageSource(const Context& context, con std::uint64_t storageCacheBytes(); +std::optional largestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { + std::optional heap; + for (std::uint32_t i = 0; i < std::min(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++i) { + if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!heap || memory.memoryHeaps[i].size > memory.memoryHeaps[*heap].size)) heap = i; + } + return heap; +} + std::uint64_t forcedCacheBudget() { static const std::uint64_t forced = [] { const char* text = std::getenv("APS5_TEXTURE_CACHE_MIB"); @@ -525,7 +533,7 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) { const auto storage = storageCacheBytes(); const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage); const bool fresh = cache.budgetDevice != context.physical; - if (const auto heap = LargestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { + if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) { char was[48] = ""; if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast(cache.reportedBudget >> 20u)); std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast(budget >> 20u), was, *heap, static_cast(reported.heapBudget[*heap] >> 20u), static_cast(reported.heapUsage[*heap] >> 20u), static_cast(sampled >> 20u), static_cast(cache.bytes >> 20u), static_cast(storage >> 20u), static_cast(storageBudget(context) >> 20u)); @@ -977,27 +985,19 @@ std::shared_ptr cachedStorageTexture(const Context& context, std } -std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { - std::optional heap; - for (std::uint32_t i = 0; i < std::min(memory.memoryHeapCount, VK_MAX_MEMORY_HEAPS); ++i) { - if ((memory.memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!heap || memory.memoryHeaps[i].size > memory.memoryHeaps[*heap].size)) heap = i; - } - return heap; -} - std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory) { - const auto heap = LargestDeviceLocalHeap(memory); + const auto heap = largestDeviceLocalHeap(memory); return std::max(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u); } std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) { - const auto heap = LargestDeviceLocalHeap(memory); + const auto heap = largestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); return std::max(512ull << 20u, std::min(TextureCacheBudget(memory), reported->heapBudget[*heap] / 4u)); } std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) { - const auto heap = LargestDeviceLocalHeap(memory); + const auto heap = largestDeviceLocalHeap(memory); if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory); const auto budget = reported->heapBudget[*heap]; const auto usage = reported->heapUsage[*heap]; @@ -1010,6 +1010,10 @@ bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::c return change > reported / 10u && sinceReport >= std::chrono::seconds(10); } +std::optional LargestDeviceLocalHeap(const VkPhysicalDeviceMemoryProperties& memory) { + return largestDeviceLocalHeap(memory); +} + TextureCacheUse TextureCacheUsage() { TextureCacheUse use; { From f1735f59f7db1d9b017d82b060d6a064dba4ad85 Mon Sep 17 00:00:00 2001 From: David Date: Thu, 15 Oct 2026 09:52:37 +0400 Subject: [PATCH 045/131] refactor(relinker): grow a moved branch's displacement with resize With the GS traps in WindowsTlsBuilder::Build, GCC 15.2 inlines this vector insert differently and reports eight -Wstringop-overflow false positives at it. resize appends the same four zero bytes and builds without them. --- core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp index faac015edb..26d5ba75b8 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsTlsBuilder.cpp @@ -187,7 +187,7 @@ PeDirectory WindowsTlsBuilder::Build(const std::vector& source, co if (!next.IsTwoByteOpcode && next.Opcode != 0xe9) { movedBytes = next.Opcode == 0xeb ? std::vector{0xe9} : std::vector{0x0f, static_cast(0x80 | (next.Opcode & 0xf))}; branchDisplacementOffset = movedBytes.size(); - movedBytes.insert(movedBytes.end(), 4, 0); + movedBytes.resize(movedBytes.size() + 4); } else { branchDisplacementOffset = next.Length - 4; } From e3ce9bac07848b9e8682845b85bf11715e497d0c Mon Sep 17 00:00:00 2001 From: screamm Date: Thu, 15 Oct 2026 09:01:16 +0200 Subject: [PATCH 046/131] fix(agc): hold a guest allocation lease while a queue stores idle results Once every queue has been idle for 50 ms, the queue 0 worker stores pending storage image results on its own (#3777, #4410). That store runs at no ordering point of the guest and took no lease of the guest allocations, so a guest unmap could run alongside it. The compare of memory outside the write watch (StorageTexture::compareUntracked) then read memory the unmap had released: the driver forgets its cached page states only when the unmap's registry mutation ends, after the memory is gone, so the range still read as mapped. The idle store now holds a lease (GuestAllocationsAcquire) under the GPU mutex, the order the other lease holders in the driver use. An unmap waits in GuestAllocationsRequireUnpinned until the store is done, and a store that starts during an unmap waits for the mutation to end, finds the range no longer registered and drops its results. ProsperoEden (PPSA99008, homebrew catalog, run without app0/eboot.bin) closes its launcher, its GL driver unmaps its direct memory at 0x200000000+0x14000000 while both 1920x1080 color buffers there still have results pending, and the queue worker faulted in compareUntracked under FlushAllPending("idle"): FATAL: unhandled exception 0xc0000005 on thread 69080 'queue worker 0x0' read of 0x0000000200000000 On main d92a8dad3 that happens in 3 of 3 cold runs. With this change 6 of 6 runs exit with code 0 and log "storage image 0x200000000+0x10e0000: memory no longer registered; 1 pending ranges dropped" for both images. New test agc_driver_idle_store_over_unmap unmaps an untracked arena surface with a pending result and releases the GPU mutex while the unmap is in progress. Without the lease the queue worker faults at the same compare (3 of 3); with it the result stays pending until the unmap ends and is then dropped. Co-Authored-By: Claude Fable 5.1 --- .../Execution/src/VulkanDevice.cpp | 1 + .../tests/execution/IdleStoreOverUnmap.cpp | 231 ++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreOverUnmap.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 9a0d342051..97f1b48cb4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -1917,6 +1917,7 @@ void VulkanDevice::StoreIdleResults(bool reapFirst) { if (!Graphics::StorageTexture::AnyPending()) return; GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush); std::lock_guard gpuLock(GuestMemory::GpuMutex()); + const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); Graphics::StorageTexture::FlushAllPending("idle"); SubmitRecorded(reapFirst); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreOverUnmap.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreOverUnmap.cpp new file mode 100644 index 0000000000..21fbc192ec --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/IdleStoreOverUnmap.cpp @@ -0,0 +1,231 @@ +#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 +#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 StoredAgain = 0x0badf00du; +constexpr auto IdleStoreWindow = std::chrono::milliseconds(500); + +alignas(256) constexpr std::array StoreCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, Stored, 0xf0200108, 0x00020102, 0xbf810000, +}; + +alignas(256) constexpr std::array StoreAgainCode{ + 0x7e040300, 0x7e060280, 0x7e0202ff, StoredAgain, 0xf0200108, 0x00020102, 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 Store(const std::uint32_t* texels, std::span code) { + std::array userData{}; + const auto texture = TextureDescriptor(texels); + std::copy(texture.begin(), texture.end(), userData.begin() + 8); + std::vector words; + 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{8, 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{static_cast(userData.size()) << 1u}; + registers(0x213, layout); + registers(0x240, userData); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + Packet packet{words.data(), static_cast(words.size()), 0, {}}; + AgcDriver::Submit(&packet, 0); +} + +std::uint32_t* MapSurface() { +#ifdef _WIN32 + auto* texels = static_cast(GuestArena::GuestArenaAllocate_nid_postfix(SurfaceBytes, Block)); + if (texels != nullptr) GuestArena::GuestArenaCommit_nid_postfix(texels, SurfaceBytes, PAGE_READWRITE, SurfaceBytes); + return texels; +#else + void* mapped = mmap(nullptr, SurfaceBytes + Block, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (mapped == MAP_FAILED) return nullptr; + 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); + auto* texels = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(texels, SurfaceBytes); + return texels; +#endif +} + +void ReleaseSurface(std::uint32_t* texels) { +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(texels, SurfaceBytes); + GuestArena::GuestArenaRelease_nid_postfix(texels, SurfaceBytes); +#else + munmap(texels, SurfaceBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(texels, SurfaceBytes); +#endif +} + +bool Holds(const std::uint32_t* texels, std::uint32_t value) { + const auto* words = static_cast(texels); + for (std::size_t index = 0; index < SurfaceBytes / 4u; ++index) { + if (words[index] == value) return true; + } + return false; +} + +bool HoldLive(std::unique_lock& hold, const std::uint32_t* texels, std::uint32_t result) { + const auto address = AddressOf(texels); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (true) { + hold.lock(); + const std::array, 1> range{{{address, address + SurfaceBytes}}}; + if (StorageTexture::FindLive(address, SurfaceBytes) != nullptr && StorageTexture::AnyPendingOverlaps(range)) return true; + hold.unlock(); + if (Holds(texels, result)) return false; + Require(std::chrono::steady_clock::now() < deadline, "idle store over unmap: the result neither became pending nor reached guest memory"); + std::this_thread::yield(); + } +} + +enum class Outcome { Settled, Missed, NotRun }; + +Outcome Run() { + auto* texels = MapSurface(); + Require(texels != nullptr, "idle store over unmap: cannot allocate the guest surface"); + std::fill(texels, texels + SurfaceBytes / 4u, Untouched); + GuestAllocations::Mutation().Add(texels, SurfaceBytes, true, true, true); + bool mapped = true; + struct Unmapped { + std::uint32_t* texels; + bool& mapped; + ~Unmapped() { + if (!mapped) return; + AgcDriverWaitIdle_nid_postfix(); + GuestAllocations::Mutation().Remove(texels); + ReleaseSurface(texels); + } + } unmapped{texels, mapped}; + Require(AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(texels))).guestBytes == SurfaceBytes, "idle store over unmap: unexpected surface size"); + const auto address = AddressOf(texels); + if (!AgcDriver::GuestMemory::Watched(address, SurfaceBytes)) { + std::printf("skipped, the surface is not write-watched\n"); + return Outcome::NotRun; + } + AgcDriver::GuestMemory::Unwatch(address, SurfaceBytes); + Require(!AgcDriver::GuestMemory::Watched(address, SurfaceBytes), "idle store over unmap: the surface is still write-watched"); + std::unique_lock hold(AgcDriver::GuestMemory::GpuMutex(), std::defer_lock); + Store(texels, StoreCode); + if (!HoldLive(hold, texels, Stored)) return Outcome::Missed; + StorageTexture::FlushAllPending("idle store over unmap fixture"); + hold.unlock(); + { + std::vector first(SurfaceBytes); + AgcDriver::GuestMemory::Read(address, first); + } + Require(static_cast(texels)[0] == Stored, "idle store over unmap: the first result was not stored"); + hold.lock(); + const auto image = StorageTexture::FindLive(address, SurfaceBytes); + const bool saved = image != nullptr && image->GuestSnapshotValid(); + hold.unlock(); + Require(image != nullptr, "idle store over unmap: the surface has no storage image"); + if (!saved) { + std::printf("skipped, the first store left no saved guest bytes to compare with\n"); + return Outcome::NotRun; + } + Store(texels, StoreAgainCode); + if (!HoldLive(hold, texels, StoredAgain)) return Outcome::Missed; + { + GuestAllocations::Mutation mutation; + mutation.Unmap(texels, SurfaceBytes, [](const void* piece, std::size_t, const void*, bool) { + ReleaseSurface(static_cast(const_cast(piece))); + }); + mapped = false; + hold.unlock(); + std::this_thread::sleep_for(IdleStoreWindow); + Require(StorageTexture::AnyPending(), "idle store over unmap: the queue's idle store ran while a guest unmap of the surface was in progress"); + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (StorageTexture::AnyPending() && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + Require(!StorageTexture::AnyPending(), "idle store over unmap: the queue's idle store did not settle the results of the unmapped surface"); + AgcDriverWaitIdle_nid_postfix(); + return Outcome::Settled; +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + struct ShutdownGuard { + ~ShutdownGuard() { try { AgcDriverShutdown_nid_postfix(); } catch (...) {} } + } shutdown; + auto outcome = Outcome::Missed; + for (std::uint32_t attempt = 0; attempt < 64u && outcome == Outcome::Missed; ++attempt) outcome = Run(); + Require(outcome != Outcome::Missed, "idle store over unmap: the result was never held pending"); + if (outcome == Outcome::NotRun) return VulkanTestSkipped; + std::puts("idle store over unmap tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From ace66bd6167f47d87183e0ee0ea09cb34bc47228 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:03:49 +0000 Subject: [PATCH 047/131] test(agc): name the overlay statistics test's tolerance check within MinGW's minwindef.h defines near and far as empty macros, so a helper named near stops compiling in any test that comes to include windows.h. The project keeps those names out of its code. --- .../tests/PerformanceOverlayStatistics.cpp | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp index aaf594a7db..b829a08c01 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlayStatistics.cpp @@ -29,7 +29,7 @@ void require(bool condition, const std::string& reason) { if (!condition) throw std::runtime_error(reason); } -bool near(double value, double expected, double tolerance = 1e-9) { +bool within(double value, double expected, double tolerance = 1e-9) { return std::abs(value - expected) <= tolerance; } @@ -39,13 +39,13 @@ void fpsWindowTests() { statistics.AddPresent(Start, 0, 0); require(statistics.Fps() == 0, "one present reads as a frame rate"); for (int i = 1; i <= 120; ++i) statistics.AddPresent(Start + i * 10ms, 0, 0); - require(near(statistics.Fps(), 100.0), "a present every 10 ms reads as " + std::to_string(statistics.Fps()) + " FPS, not 100"); + require(within(statistics.Fps(), 100.0), "a present every 10 ms reads as " + std::to_string(statistics.Fps()) + " FPS, not 100"); statistics.AddPresent(Start + 1700ms, 0, 0); - require(near(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51"); + require(within(statistics.Fps(), 51.0), "after a 500 ms stall the last second holds 52 presents, read as " + std::to_string(statistics.Fps()) + " FPS, not 51"); statistics.AddPresent(Start + 3700ms, 0, 0); - require(near(statistics.Fps(), 0.5), "after a 2 s stall the last second holds one present, read as " + std::to_string(statistics.Fps()) + " FPS, not the last frame's 0.5"); + require(within(statistics.Fps(), 0.5), "after a 2 s stall the last second holds one present, read as " + std::to_string(statistics.Fps()) + " FPS, not the last frame's 0.5"); statistics.AddPresent(Start + 3750ms, 0, 0); - require(near(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20"); + require(within(statistics.Fps(), 20.0), "two presents 50 ms apart read as " + std::to_string(statistics.Fps()) + " FPS, not 20"); bool refused = false; try { statistics.AddPresent(Start + 3740ms, 0, 0); @@ -66,24 +66,24 @@ void frameRingTests() { statistics.AddPresent(at, 0, 0); } const auto partial = statistics.FrameTimes(); - require(partial.size() == 4 && near(partial[0], 1) && near(partial[3], 4), "the frame times of a short run are not in present order"); + require(partial.size() == 4 && within(partial[0], 1) && within(partial[3], 4), "the frame times of a short run are not in present order"); for (int i = 5; i <= 150; ++i) { at += std::chrono::milliseconds(i); statistics.AddPresent(at, 0, 0); } const auto times = statistics.FrameTimes(); require(times.size() == OverlayStatistics::FrameHistory, "the ring holds " + std::to_string(times.size()) + " frames, not 120"); - for (std::size_t i = 0; i < times.size(); ++i) require(near(times[i], 31.0 + static_cast(i)), "frame " + std::to_string(i) + " of the ring is " + std::to_string(times[i]) + " ms"); - require(near(statistics.LastFrameMs(), 150), "the last frame is not the newest"); + for (std::size_t i = 0; i < times.size(); ++i) require(within(times[i], 31.0 + static_cast(i)), "frame " + std::to_string(i) + " of the ring is " + std::to_string(times[i]) + " ms"); + require(within(statistics.LastFrameMs(), 150), "the last frame is not the newest"); const auto range = statistics.FrameTimeRange(); - require(near(range.minimum, 31) && near(range.maximum, 150) && near(range.average, 90.5), "the graph range of 31..150 ms reads as " + std::to_string(range.minimum) + "/" + std::to_string(range.average) + "/" + std::to_string(range.maximum)); + require(within(range.minimum, 31) && within(range.maximum, 150) && within(range.average, 90.5), "the graph range of 31..150 ms reads as " + std::to_string(range.minimum) + "/" + std::to_string(range.average) + "/" + std::to_string(range.maximum)); } void counterTests() { OverlayStatistics statistics; constexpr std::uint64_t Earlier = 1'000'000'000u; for (std::uint64_t i = 0; i <= 60; ++i) statistics.AddPresent(Start + i * 16ms, Earlier + i * 4'000'000u, Earlier + i * 1'500'000u); - require(near(statistics.GpuMsPerFrame(), 4.0) && near(statistics.DriverMsPerFrame(), 1.5), "4 ms of GPU and 1.5 ms of driver time per frame read as " + std::to_string(statistics.GpuMsPerFrame()) + " and " + std::to_string(statistics.DriverMsPerFrame())); + require(within(statistics.GpuMsPerFrame(), 4.0) && within(statistics.DriverMsPerFrame(), 1.5), "4 ms of GPU and 1.5 ms of driver time per frame read as " + std::to_string(statistics.GpuMsPerFrame()) + " and " + std::to_string(statistics.DriverMsPerFrame())); statistics.AddPresent(Start + 61 * 16ms, 5, 5); require(statistics.GpuMsPerFrame() == 0 && statistics.DriverMsPerFrame() == 0, "a counter that went back is read as time"); } @@ -107,8 +107,8 @@ void lowRateTests() { gpuTotal += Gpu[i]; driverTotal += Driver[i]; slow.AddPresent(Start + (i + 1) * 2s, gpuTotal, driverTotal); - require(near(slow.Fps(), 0.5) && near(slow.LastFrameMs(), 2000), "a present every 2 s reads as " + std::to_string(slow.Fps()) + " FPS and " + std::to_string(slow.LastFrameMs()) + " ms, not 0.5 and 2000"); - require(near(slow.GpuMsPerFrame(), static_cast(Gpu[i]) / 1e6) && near(slow.DriverMsPerFrame(), static_cast(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own"); + require(within(slow.Fps(), 0.5) && within(slow.LastFrameMs(), 2000), "a present every 2 s reads as " + std::to_string(slow.Fps()) + " FPS and " + std::to_string(slow.LastFrameMs()) + " ms, not 0.5 and 2000"); + require(within(slow.GpuMsPerFrame(), static_cast(Gpu[i]) / 1e6) && within(slow.DriverMsPerFrame(), static_cast(Driver[i]) / 1e6), "frame " + std::to_string(i + 1) + " every 2 s reads " + std::to_string(slow.GpuMsPerFrame()) + " ms of GPU and " + std::to_string(slow.DriverMsPerFrame()) + " ms of driver time, not its own"); } const auto everyTwoSeconds = AgcDriver::ComposeOverlay(slow, true, true); require(everyTwoSeconds.above == std::vector{"PROFILED", "FPS: 0.50", "FRAME: 2000 MS"}, "a present every 2 s shows " + std::to_string(everyTwoSeconds.above.size()) + " lines above the graph, the last '" + everyTwoSeconds.above.back() + "'"); @@ -117,7 +117,7 @@ void lowRateTests() { OverlayStatistics glacial; glacial.AddPresent(Start, 0, 0); for (int i = 1; i <= 5; ++i) glacial.AddPresent(Start + i * 12s, static_cast(i) * 250'000'000u, static_cast(i) * 12'500'000'000u); - require(near(glacial.Fps(), 1000.0 / 12000.0) && near(glacial.LastFrameMs(), 12000) && near(glacial.GpuMsPerFrame(), 250) && near(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms"); + require(within(glacial.Fps(), 1000.0 / 12000.0) && within(glacial.LastFrameMs(), 12000) && within(glacial.GpuMsPerFrame(), 250) && within(glacial.DriverMsPerFrame(), 12500), "a present every 12 s reads as " + std::to_string(glacial.Fps()) + " FPS, " + std::to_string(glacial.LastFrameMs()) + " ms, " + std::to_string(glacial.GpuMsPerFrame()) + " and " + std::to_string(glacial.DriverMsPerFrame()) + " ms"); const auto everyTwelveSeconds = AgcDriver::ComposeOverlay(glacial, true, true); require(everyTwelveSeconds.above == std::vector{"PROFILED", "FPS: 0.08", "FRAME: 9999 MS"}, "a present every 12 s shows " + std::to_string(everyTwelveSeconds.above.size()) + " lines above the graph, the last '" + everyTwelveSeconds.above.back() + "'"); require(everyTwelveSeconds.below[0] == "MIN/AVG/MAX: 9999/9999/9999" && everyTwelveSeconds.below[1] == "GPU: 250 MS" && everyTwelveSeconds.below[2] == "DRIVER: 9999 MS", "a present every 12 s shows '" + everyTwelveSeconds.below[0] + "', '" + everyTwelveSeconds.below[1] + "' and '" + everyTwelveSeconds.below[2] + "'"); @@ -141,7 +141,7 @@ void lowRateTests() { rising.AddPresent(at, total, 0); }; const auto check = [&](double fps, double gpuMs, const char* shown, const std::string& when) { - require(near(rising.Fps(), fps) && near(rising.GpuMsPerFrame(), gpuMs) && AgcDriver::FormatOverlayRate(rising.Fps()) == shown, when + " reads as " + std::to_string(rising.Fps()) + " FPS ('" + AgcDriver::FormatOverlayRate(rising.Fps()) + "') and " + std::to_string(rising.GpuMsPerFrame()) + " ms of GPU time, not " + std::to_string(fps) + " ('" + shown + "') and " + std::to_string(gpuMs)); + require(within(rising.Fps(), fps) && within(rising.GpuMsPerFrame(), gpuMs) && AgcDriver::FormatOverlayRate(rising.Fps()) == shown, when + " reads as " + std::to_string(rising.Fps()) + " FPS ('" + AgcDriver::FormatOverlayRate(rising.Fps()) + "') and " + std::to_string(rising.GpuMsPerFrame()) + " ms of GPU time, not " + std::to_string(fps) + " ('" + shown + "') and " + std::to_string(gpuMs)); }; step(2s, 40'000'000u); check(0.5, 40, "0.50", "a 2 s frame"); @@ -310,16 +310,16 @@ AgcDriver::OverlayDetails fullDetails() { void packetStatisticsTests() { const auto statistics = countedStatistics(); - require(near(statistics.DrawsPerFrame(), 1240) && near(statistics.DispatchesPerFrame(), 85), "1240 draws and 85 dispatches per frame read as " + std::to_string(statistics.DrawsPerFrame()) + " and " + std::to_string(statistics.DispatchesPerFrame())); + require(within(statistics.DrawsPerFrame(), 1240) && within(statistics.DispatchesPerFrame(), 85), "1240 draws and 85 dispatches per frame read as " + std::to_string(statistics.DrawsPerFrame()) + " and " + std::to_string(statistics.DispatchesPerFrame())); require(statistics.PipelinesInWindow() == 3 && statistics.Latest().pipelines == 1503, "3 pipelines created within the last second of 1503 read as " + std::to_string(statistics.PipelinesInWindow()) + " of " + std::to_string(statistics.Latest().pipelines)); - require(near(statistics.GpuMsPerFrame(), 4.0) && near(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters"); + require(within(statistics.GpuMsPerFrame(), 4.0) && within(statistics.DriverMsPerFrame(), 1.5), "the time counters changed with the packet counters"); auto stalled = statistics; auto counters = stalled.Latest(); counters.draws += 900; counters.dispatches += 40; counters.pipelines += 7; stalled.AddPresent(Start + 3000ms, counters); - require(near(stalled.DrawsPerFrame(), 900) && near(stalled.DispatchesPerFrame(), 40) && stalled.PipelinesInWindow() == 7 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as " + std::to_string(stalled.DrawsPerFrame()) + " draws, " + std::to_string(stalled.DispatchesPerFrame()) + " dispatches and " + std::to_string(stalled.PipelinesInWindow()) + " pipelines, not the last frame's 900, 40 and 7"); + require(within(stalled.DrawsPerFrame(), 900) && within(stalled.DispatchesPerFrame(), 40) && stalled.PipelinesInWindow() == 7 && stalled.Latest().pipelines == 1510, "a present alone in its second reads as " + std::to_string(stalled.DrawsPerFrame()) + " draws, " + std::to_string(stalled.DispatchesPerFrame()) + " dispatches and " + std::to_string(stalled.PipelinesInWindow()) + " pipelines, not the last frame's 900, 40 and 7"); auto restarted = statistics; counters = restarted.Latest(); counters.draws = 3; @@ -465,9 +465,9 @@ void paletteTests() { const auto sdr = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); require(sdr[1] == std::array{1.0f, 1.0f, 1.0f, 1.0f} && sdr[0][3] > 0.0f && sdr[0][3] < 1.0f, "the SDR palette is not opaque white text over a translucent panel"); const auto srgb = AgcDriver::OverlayPalette(VK_FORMAT_B8G8R8A8_SRGB, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR); - require(near(srgb[2][1], std::pow((0.8 + 0.055) / 1.055, 2.4), 1e-5) && srgb[1][0] == 1.0f, "an sRGB swapchain gets encoded colors"); + require(within(srgb[2][1], std::pow((0.8 + 0.055) / 1.055, 2.4), 1e-5) && srgb[1][0] == 1.0f, "an sRGB swapchain gets encoded colors"); const auto hdr = AgcDriver::OverlayPalette(VK_FORMAT_A2B10G10R10_UNORM_PACK32, VK_COLOR_SPACE_HDR10_ST2084_EXT); - for (std::size_t channel = 0; channel < 3; ++channel) require(near(hdr[1][channel], 0.5807, 0.0015), "HDR10 white text is not at 203 nits (PQ " + std::to_string(hdr[1][channel]) + ")"); + for (std::size_t channel = 0; channel < 3; ++channel) require(within(hdr[1][channel], 0.5807, 0.0015), "HDR10 white text is not at 203 nits (PQ " + std::to_string(hdr[1][channel]) + ")"); require(std::lround(hdr[0][0] * 1023.0f) == 0 && hdr[0][3] == sdr[0][3], "the HDR10 panel is not black at the same opacity"); } From 70c88b3e0fb15e5b2350bce88367bd03482ee54e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 01:48:16 -0700 Subject: [PATCH 048/131] docs: say that the overlay's DRIVER line can exceed FRAME with several workers --- docs/user/USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 65a2a146b5..58e136b1fc 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -209,7 +209,7 @@ F10 shows or hides a performance overlay over the presented image; `ANYPS5_OVERL | graph | The last 120 frame times, the newest on the right. The full height is the longest of them or twice their average, whichever is larger. | | `MIN/AVG/MAX` | The shortest, average and longest of the 120 frame times, in ms. | | `GPU` | GPU time per frame over the last second, or of the last frame below 1 fps, in ms: the span of each recorded batch from its first to its last command, from the timestamp queries of `APS5_PROFILE_GPU=1`. Shown only with that variable. Batches are counted when they complete, so the line trails a sudden change by up to a second, or by a frame below 1 fps. Batches that overlap on different queues each count in full, so the line can exceed `FRAME`. | -| `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. | +| `DRIVER` | CPU time per frame over the last second, or of the last frame below 1 fps, in ms, that the packet workers spent in dispatch and draw packets (the `dispatch` and `draw` time of the `[gpu] worker` line of `APS5_PROFILE_DRAW=1`). Shown only with that variable. Every worker's time counts in full, so with several workers busy at once the line can exceed `FRAME`. | | `DRAWS` | Draw packets per frame over the last second, or of the last frame below 1 fps, then dispatch packets per frame (`DISPATCH`), as the packet workers execute them; packets that predication skips are not counted. Shown once the title has submitted either. | | `SHADERS` | Pipelines created in the last second, or during the last frame below 1 fps, then since the start (`TOTAL`): a graphics pipeline for each draw state the pipeline cache had not seen and a compute pipeline for each new dispatch shader. A rise while the game stutters points at pipeline compilation. Shown once a pipeline was created. | | `VRAM` | Usage and budget of the GPU's largest device-local memory heap from `VK_EXT_memory_budget`, read once a second. Not shown when the driver lacks the extension. | From e09870006df4cb88fa200eedd0f75d25d1fdf14e Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 04:35:02 -0700 Subject: [PATCH 049/131] refactor(agc): add the overlay's driver time after the worker profile report #6292 moves the APS5_PROFILE_DRAW check in Driver::timed to the top and rewrites the report's condition, the two lines this branch's driver-time line sat between, so the two conflicted. The line now follows the report. It reads the same flag and times, so it counts the same. --- .../Execution/src/Driver/Packets/PacketExecution.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ef2313522c..043646aac7 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -24,11 +24,11 @@ void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { const auto end = std::chrono::steady_clock::now(); profile.*bucket += std::chrono::duration(end - begin).count(); static const bool report = std::getenv("APS5_PROFILE_DRAW") != nullptr; - if (report && bucket != &WorkerProfile::waitMs) PerformanceCounters::AddDriverNanoseconds(static_cast(std::chrono::duration_cast(end - begin).count())); if (report && end - profile.reported > std::chrono::seconds(10)) { profile.reported = end; AgcDriver::ProfilePrint_nid_no_patch( "[gpu] worker at %.0f s: dispatch %.1f s, draw %.1f s, wait %.1f s\n", std::chrono::duration(end - profile.start).count(), profile.dispatchMs / 1000, profile.drawMs / 1000, profile.waitMs / 1000); } + if (report && bucket != &WorkerProfile::waitMs) PerformanceCounters::AddDriverNanoseconds(static_cast(std::chrono::duration_cast(end - begin).count())); } void Driver::execute(const Submission& submission) { From f7ec358ec4c8f63c0c39a8a31d327389410083c6 Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:57:49 +0300 Subject: [PATCH 050/131] test(relinker): convert the SCE module extent fixtures for every target The two fixtures were converted for Windows only. They now run for Linux, Windows and macOS. The syscall scan of a module with data after its code fails on all three without the fix, and the loader path without PT_INTERP fails on Windows and macOS, so the macOS code analysis stays under test. --- .../relinker/tests/test_sce_module_code_extent.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/relinker/relinker/tests/test_sce_module_code_extent.py b/core/relinker/relinker/tests/test_sce_module_code_extent.py index 28a19c721e..9395a04db3 100644 --- a/core/relinker/relinker/tests/test_sce_module_code_extent.py +++ b/core/relinker/relinker/tests/test_sce_module_code_extent.py @@ -123,22 +123,22 @@ def sce_module(): return image -def run_module(relinker, work, name, module): +def run_module(relinker, work, name, module, flags): case = work / name (case / "sce_module").mkdir(parents=True) (case / "input.elf").write_bytes(importing_executable([])) (case / "sce_module" / "libExample.prx").write_bytes(module) - result = subprocess.run([str(relinker), "--windows", str(case / "input.elf"), str(case / "output.exe")], + result = subprocess.run([str(relinker), *flags, str(case / "input.elf"), str(case / "output.exe")], capture_output=True, text=True, timeout=30) assert result.returncode == 0 and (case / "app0" / "sce_module" / "libExample.prx.guest.prx").exists(), ( name, result.stdout, result.stderr) -def run(relinker, work, name, image, expected_error=None): +def run(relinker, work, name, image, flags, expected_error=None): source = work / (name + ".elf") output = work / (name + ".exe") source.write_bytes(image) - result = subprocess.run([str(relinker), "--skip-sce-module", "--windows", str(source), str(output)], + result = subprocess.run([str(relinker), "--skip-sce-module", *flags, str(source), str(output)], capture_output=True, text=True, timeout=20) if expected_error is None: assert result.returncode == 0 and output.exists(), (name, result.stdout, result.stderr) @@ -151,8 +151,9 @@ def main(): relinker = Path(sys.argv[1]).resolve() with tempfile.TemporaryDirectory(prefix="anyps5-sce-module-code-extent-") as directory: work = Path(directory) - run(relinker, work, "sce-loader-path-without-interp-header", fixture(rodata_address=False, interp_header=False)) - run_module(relinker, work, "sce-module-data-after-code", sce_module()) + for system, flags in (("linux", []), ("windows", ["--windows"]), ("macos", ["--macos"])): + run(relinker, work, "sce-loader-path-without-interp-header-" + system, fixture(rodata_address=False, interp_header=False), flags) + run_module(relinker, work, "sce-module-data-after-code-" + system, sce_module(), flags) print("SCE module code extent tests passed") From 2852249eb92d63d4cbfcacc6ed65fe4241963593 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Thu, 15 Oct 2026 01:16:36 +0300 Subject: [PATCH 051/131] fix(libc): open closed standard descriptors on /dev/null when libc loads --- core/libs/prx/libc/src/StandardStreams.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 8d136d05b4..c18cf23b37 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -4,10 +4,26 @@ #ifdef _WIN32 #include #else +#include #include #endif -namespace { FileStream input{stdin}; } +namespace { +#ifndef _WIN32 +const bool standardDescriptorsOpen = [] { + for (int descriptor = 0; descriptor <= 2; ++descriptor) { + if (::fcntl(descriptor, F_GETFD) != -1 || errno != EBADF) continue; + const int opened = ::open("/dev/null", descriptor == 0 ? O_RDONLY : O_WRONLY); + if (opened > descriptor) { + ::dup2(opened, descriptor); + ::close(opened); + } + } + return true; +}(); +#endif +FileStream input{stdin}; +} extern "C" { FileStream* __stdinp_nid_postfix = &input; FileStream* __stdoutp_nid_postfix = &_Stdout_nid_postfix; From 7c4400e926195c8b84d7f1be8d95a32f5c9c4fe1 Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Thu, 15 Oct 2026 01:16:36 +0300 Subject: [PATCH 052/131] test(libc): start a guest process with its standard descriptors closed --- core/libs/prx/libkernel/CMakeLists.txt | 5 ++ .../tests/GuestClosedStandardDescriptors.cpp | 66 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 core/libs/tests/GuestClosedStandardDescriptors.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 21cf02d628..1a87daace5 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -265,6 +265,11 @@ if(BUILD_TESTING) target_link_libraries(guest_host_standard_error_tests PRIVATE libkernel libc) add_test(NAME guest_host_standard_error COMMAND guest_host_standard_error_tests) set_tests_properties(guest_host_standard_error PROPERTIES TIMEOUT 10) + add_test_executable(guest_closed_standard_descriptors_tests ${LIBS_INCLUDE_DIR}/tests/GuestClosedStandardDescriptors.cpp) + target_include_directories(guest_closed_standard_descriptors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_closed_standard_descriptors_tests PRIVATE libkernel libc) + add_test(NAME guest_closed_standard_descriptors COMMAND guest_closed_standard_descriptors_tests) + set_tests_properties(guest_closed_standard_descriptors PROPERTIES TIMEOUT 10) endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/tests/GuestClosedStandardDescriptors.cpp b/core/libs/tests/GuestClosedStandardDescriptors.cpp new file mode 100644 index 0000000000..4cb0deece1 --- /dev/null +++ b/core/libs/tests/GuestClosedStandardDescriptors.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI fcntl_nid_postfix(int, int, ...); +} + +namespace { + +constexpr int GuestGetFlags = 3; + +int Child(const char* closed) { + struct stat null{}; + if (::stat("/dev/null", &null) != 0) return 70; + for (int descriptor = 0; descriptor <= 2; ++descriptor) { + struct stat status{}; + if (::fstat(descriptor, &status) != 0) return 71 + descriptor; + if (closed[descriptor] != '1') continue; + if (!S_ISCHR(status.st_mode) || status.st_rdev != null.st_rdev) return 74 + descriptor; + if ((::fcntl(descriptor, F_GETFL) & O_ACCMODE) != (descriptor == 0 ? O_RDONLY : O_WRONLY)) return 77 + descriptor; + } + if (closed[0] == '1' && fcntl_nid_postfix(0, GuestGetFlags) < 0) return 80; + return 0; +} + +int RunClosed(const char* self, bool closeInput, bool closeOutput, bool closeError) { + const pid_t child = ::fork(); + if (child < 0) return -1; + if (child == 0) { + if (closeInput) ::close(0); + if (closeOutput) ::close(1); + if (closeError) ::close(2); + char closed[] = {closeInput ? '1' : '0', closeOutput ? '1' : '0', closeError ? '1' : '0', '\0'}; + char* const arguments[] = {const_cast(self), const_cast("--closed-child"), closed, nullptr}; + ::execv(self, arguments); + ::_exit(69); + } + int status = 0; + if (::waitpid(child, &status, 0) != child) return -1; + return WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status); +} + +} + +int main(int argc, char** argv) { + if (argc > 2 && std::strcmp(argv[1], "--closed-child") == 0 && std::strlen(argv[2]) == 3) return Child(argv[2]); + const struct { bool input, output, error; const char* name; } cases[] = { + {true, false, false, "stdin"}, + {false, true, false, "stdout"}, + {true, true, true, "all three"}, + }; + for (const auto& test : cases) { + const int result = RunClosed(argv[0], test.input, test.output, test.error); + if (result != 0) { + std::fprintf(stderr, "closed standard descriptors, %s: child exited with %d\n", test.name, result); + return 1; + } + } + std::puts("closed standard descriptors are reopened on /dev/null"); + return 0; +} From a7ef31e103a08f45ead7697b378d6773baee4e8b Mon Sep 17 00:00:00 2001 From: Yossi Yadgar Date: Thu, 15 Oct 2026 01:16:36 +0300 Subject: [PATCH 053/131] docs(debt): note how closed standard descriptors are reopened --- 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 50359fc84c..84d23a1f95 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -39,6 +39,7 @@ - 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. - [getc_unlocked, putc_unlocked, getchar_unlocked, putchar_unlocked](../../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [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. - [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 - [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. From 68311f58245b40826ad9dc0ec565669873a0b435 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 15:36:23 +0000 Subject: [PATCH 054/131] fix(agc): size sampled MSAA surface footprints --- .../Graphics/src/TextureTiling.cpp | 20 +++++++++++++++++++ .../tests/GuestTextureResource.cpp | 1 + 2 files changed, 21 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 6eca2ae659..24429b8a72 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -405,6 +405,26 @@ SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { geometry.layerBytes = thick.slabBytes; return geometry; } + if (descriptor.dimension == TextureDimension::k2DMsaa) { + Require(descriptor.samples > 1u && std::has_single_bit(descriptor.samples), "multisampled texture has an invalid sample count"); + const auto bytesPerElement = BytesPerElement(descriptor.format); + Require(std::has_single_bit(bytesPerElement), "multisampled texture format has an unsupported element size"); + const auto block = GetBlockLayout(descriptor.tileMode, bytesPerElement); + const auto log2Samples = static_cast(std::countr_zero(descriptor.samples)); + const auto log2Pixels = static_cast(std::countr_zero(block.blockSize)) - static_cast(std::countr_zero(bytesPerElement)) - log2Samples; + const auto log2Width = (log2Pixels + ((log2Samples & 1u) == 0u ? 1u : 0u)) / 2u; + const auto blockWidth = 1u << log2Width; + const auto blockHeight = 1u << (log2Pixels - log2Width); + const auto align = [](std::uint32_t extent, std::uint32_t blockDimension) { + return (static_cast(extent) + blockDimension - 1u) / blockDimension * blockDimension; + }; + geometry.guestBytes = align(descriptor.width, blockWidth) * align(descriptor.height, blockHeight) * bytesPerElement * descriptor.samples; + geometry.layerBytes = geometry.guestBytes; + geometry.layers = 1; + geometry.imageLayers = 1; + geometry.imageDepth = 1; + return geometry; + } geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount); const bool layered = descriptor.dimension == TextureDimension::k2DArray || descriptor.dimension == TextureDimension::k1DArray || descriptor.dimension == TextureDimension::kCube; geometry.layers = layered ? descriptor.depthOrLastArray + 1u : 1u; diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 6a8e0d8e7a..8b3e9cbc07 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -266,6 +266,7 @@ void RunGuestTextureResourceTests() { const std::array canvas{0x021f2000u, 0x03800000u, 0x804a004au, 0xe1b20facu, 0x00000000u, 0x00400020u, 0x00000000u, 0x00000000u}; result = DecodeTextureResource(canvas); Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.baseAddress == 0x21f200000ull && result.width == 297 && result.height == 297 && result.format == 56 && result.tileMode == TextureTileMode::kR64KBX, "the 4x canvas descriptor of PPSA99006 decoded incorrectly"); + Require(DescribeSurface(result).guestBytes == 0x190000, "4x render-target geometry must include fragment-aware padding for the PPSA99006 canvas"); Fields fewerFragments = multisampled; fewerFragments.maxMip = 1; rejectFields(fewerFragments, "same sample count"); From 49919d7f32f3115d82b9f8321c443d09ebbd0b2e Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Thu, 15 Oct 2026 18:56:30 +0200 Subject: [PATCH 055/131] fix(agc): drop resource-cache entries that hold released sampled textures A resource-cache entry keeps every sampled texture of its descriptor sets alive. The sampled texture cache passes over textures something else holds and, past its ceiling, drops them anyway; textures it makes without caching are never in it. Entries holding either kept them alive with no budget counting them, bounded only by the 1,024-entry and 65,536-element counts, until device memory ran out. The texture cache now marks such textures released. ResourceCache::Insert refuses descriptor sets holding one and hands the entries that hold one back to the caller, like RemoveImage does for evicted storage images. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/include/ShaderResources.hpp | 2 + .../Graphics/include/Texture.hpp | 3 ++ .../Graphics/src/ShaderResources.cpp | 32 +++++++++++++- .../tests/execution/SampledCacheBudget.cpp | 43 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 5 files changed, 80 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index efefda3a7f..409580699e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -196,6 +196,7 @@ public: std::vector> DeviceReads() const { return guestMemory.DeviceReads(); } bool ReadsImage(const StorageTexture* image) const; bool HasCachedStorageImages() const; + bool HoldsReleasedTexture() const; std::uint32_t TableElements() const { return tableElements[0] + tableElements[1]; } const std::vector& LayoutKey() const { return layoutKey; } const std::vector& FragmentLayoutKey() const { return fragmentLayoutKey; } @@ -634,6 +635,7 @@ public: private: void erase(const Key& key); void noteMiss(const Key& key); + void dropReleasedTextures(std::vector>* evicted); struct KeyHash { std::size_t operator()(const Key& key) const noexcept { std::uint64_t hash = 14695981039346656037ull; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index bc6efaf720..3ee21eb426 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -109,6 +109,8 @@ public: PerUseCopy CopyKind() const { return perUseCopy; } bool RefreshedPerUse() const { return perUseCopy == PerUseCopy::RemadePerUse; } void MarkRefreshedPerUse() { perUseCopy = PerUseCopy::RemadePerUse; } + bool Released() const { return released.load(std::memory_order_acquire); } + void MarkReleased() { released.store(true, std::memory_order_release); } private: void release() noexcept; @@ -142,6 +144,7 @@ private: std::shared_ptr storageSource; std::unique_ptr upload; PerUseCopy perUseCopy = PerUseCopy::None; + std::atomic released{false}; bool copiedOnce = false; std::uint64_t copiedVersion = 0; VkFormat depthFormat = VK_FORMAT_UNDEFINED; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 15a0f3c2d2..dced3d585e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -172,6 +172,16 @@ void noteUncached(const std::shared_ptr& image) { uncached.any.store(true, std::memory_order_release); } +std::atomic& TexturesReleased() { + static std::atomic released{false}; + return released; +} + +void noteReleased(const std::shared_ptr& texture) { + texture->MarkReleased(); + TexturesReleased().store(true, std::memory_order_release); +} + bool SamplerCacheDisabled() { static const bool disabled = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr; return disabled; @@ -203,6 +213,7 @@ void eraseTexture(TextureCache& cache, std::list::iterator it) { } } } + if (it->texture.use_count() > 1) noteReleased(it->texture); cache.index.erase(it->key); cache.entries.erase(it); ++cache.erasures; @@ -797,6 +808,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span resources, std::vector>* evicted) { std::lock_guard lock(mutex); - if (!resources->HasCachedStorageImages()) return; + dropReleasedTextures(evicted); + if (!resources->HasCachedStorageImages() || resources->HoldsReleasedTexture()) return; if (const auto found = index.find(key); found != index.end()) { if (evicted != nullptr) evicted->push_back(std::move(found->second->second)); entries.erase(found->second); @@ -3316,6 +3329,19 @@ void ResourceCache::RemoveImage(const StorageTexture* image, std::vector>* evicted) { + if (!TexturesReleased().exchange(false, std::memory_order_acq_rel)) return; + for (auto it = entries.begin(); it != entries.end();) { + if (!it->second->HoldsReleasedTexture()) { + ++it; + continue; + } + if (evicted != nullptr) evicted->push_back(std::move(it->second)); + index.erase(it->first); + it = entries.erase(it); + } +} + bool ResourceCache::Touch(const Key& key) { resourceCacheTouches.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(mutex); @@ -4719,6 +4745,10 @@ bool ShaderResources::HasCachedStorageImages() const { return std::none_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->StorageSource() != nullptr && !texture->StorageSource()->Cached(); }); } +bool ShaderResources::HoldsReleasedTexture() const { + return std::any_of(textures.begin(), textures.end(), [](const auto& texture) { return texture != nullptr && texture->Released(); }); +} + bool ShaderResources::ReadsImage(const StorageTexture* image) const { if (image == nullptr) return false; for (const auto& texture : textures) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 9d1978dbb2..d8a6076a3a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -424,6 +424,48 @@ void heldTests(const Context& context) { Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "released textures were not evicted down to the budget"); } +void releasedTests(Context context) { + PlaceholderImages placeholders(context); + context.placeholderImages = &placeholders; + TextureCache textures(context); + context.textureCache = &textures; + ClearCachedTextures(context.device); + auto& cache = SharedResourceCache(); + cache.Clear(); + try { + Block block(context); + auto pinned = ImageResources(context, block, 0, 1, true); + Require(pinned->SampledTextures().size() == 1 && !pinned->SampledTextures().front()->ViewsStorageImage(), "the pinned descriptor set does not hold a sampled snapshot"); + const std::weak_ptr texture = pinned->SampledTextures().front(); + const std::weak_ptr pinnedSet = pinned; + cache.Insert({1}, pinned); + Require(cache.Find({1}) == pinned, "the resource cache rejected a descriptor set whose texture is cached"); + pinned.reset(); + ClearCachedTextures(context.device); + Require(!texture.expired() && texture.lock()->Released(), "the sampled texture cache dropped a texture the resource cache holds without marking it released"); + auto live = ImageResources(context, block, 1, 1, true); + std::vector> displaced; + cache.Insert({2}, live, &displaced); + Require(cache.Find({1}) == nullptr && cache.Find({2}) == live && cache.Size() == 1, "the resource cache kept an entry holding a released texture"); + Require(displaced.size() == 1 && displaced.front() == pinnedSet.lock(), "the entry holding a released texture was not handed back to the caller"); + displaced.clear(); + Require(pinnedSet.expired() && texture.expired(), "the resource cache kept a released texture alive"); + auto stale = ImageResources(context, block, 2, 1, true); + ClearCachedTextures(context.device); + Require(stale->SampledTextures().front()->Released(), "a held texture the sampled cache dropped is not marked released"); + cache.Insert({3}, stale, &displaced); + Require(cache.Find({3}) == nullptr, "a descriptor set holding a released texture entered the resource cache"); + Require(cache.Size() == 0 && displaced.size() == 1 && displaced.front() == live, "the live entry whose texture was released stayed in the resource cache"); + displaced.clear(); + } catch (...) { + cache.Clear(); + throw; + } + cache.Clear(); + ClearCachedTextures(context.device); + std::puts("resource cache released texture tests passed"); +} + void rescanTests(const Context& context) { constexpr std::uint32_t SmallSide = 128; constexpr std::size_t SmallBytes = std::size_t{SmallSide} * SmallSide * 4u; @@ -638,6 +680,7 @@ int main(int argc, char** argv) { reportedBudgetTests(context); snapshotTests(context); heldTests(context); + releasedTests(context); rescanTests(context); viewEvictionTests(context); viewTests(context); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 3edcde24ff..4dce0d2b6e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -150,3 +150,4 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. +- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (up to 1,024, each with its textures); that cost is not measured. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344), and before this change with the env bounds lowered as a stand-in: with 256 entries and 16,384 table elements, sampled textures held outside the cache fell from up to 7,830 MiB to 2,541 MiB at the wolf scene and device-local heap 0 stayed under its 23 GB budget. From 349420af7fb2ed7c738d0eefb54d0d4744172026 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Thu, 15 Oct 2026 19:28:43 +0200 Subject: [PATCH 056/131] docs(agc): record the measured effect of dropping entries with released textures Co-Authored-By: Claude Opus 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 4dce0d2b6e..5662ba6a3e 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -150,4 +150,4 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. -- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (up to 1,024, each with its textures); that cost is not measured. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344), and before this change with the env bounds lowered as a stand-in: with 256 entries and 16,384 table elements, sampled textures held outside the cache fell from up to 7,830 MiB to 2,541 MiB at the wolf scene and device-local heap 0 stayed under its 23 GB budget. +- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (up to 1,024, each with its textures); that cost is not measured. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344) on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. From ce316bfce8ed8a71b49a36b6b6b6b456860c8d0b Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:33:25 -0300 Subject: [PATCH 057/131] fix(relinker): validate the PT_TLS layout when reading program headers The Linux output passed a PT_TLS whose initialization image is outside every PT_LOAD, whose file size exceeds its memory size, whose alignment is not a power of two, or that follows another PT_TLS, straight to the dynamic loader, which reads the image from p_vaddr when it sets up TLS. The Windows and macOS writers already refuse these layouts. Reject them in ElfReader so every target, and guest modules, see the same rule. An empty PT_TLS placeholder is still accepted. --- .../relinker/src/parsing/ElfReader.cpp | 18 ++++++++ .../relinker/tests/ElfReaderBoundsTests.cpp | 42 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index b82abed93f..e915e0f2c5 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -165,6 +166,23 @@ std::vector ElfReader::ReadProgramHeaders() const { } } + bool hasTls = false; + for (const auto& tls : headers) { + if (tls.Type != PT_TLS) continue; + if (hasTls) + throw RelinkerException("Multiple ELF TLS segments", tls.Offset); + hasTls = true; + if (tls.FileSize > tls.MemorySize || (tls.Alignment > 1 && !std::has_single_bit(tls.Alignment))) + throw RelinkerException("Invalid or unsupported ELF TLS layout", tls.Offset); + if (tls.FileSize == 0) continue; + const bool covered = std::any_of(headers.begin(), headers.end(), [&](const ProgramHeader& load) { + return load.Type == PT_LOAD && tls.MappedAddress >= load.MappedAddress && tls.MappedAddress - load.MappedAddress <= load.MemorySize + && tls.FileSize <= load.MemorySize - (tls.MappedAddress - load.MappedAddress); + }); + if (!covered) + throw RelinkerException("PT_TLS initialization image is outside PT_LOAD memory", tls.Offset); + } + return headers; } diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp index ef54dd3c96..1597cf6807 100644 --- a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -54,6 +55,22 @@ Bytes Fixture() { return bytes; } +Bytes TlsFixture(const std::uint64_t address, const std::uint64_t fileSize, const std::uint64_t memorySize, const std::uint64_t alignment) { + auto bytes = Fixture(); + std::memcpy(bytes.data() + 0x260, bytes.data() + 64, 56); + Write(bytes, 32, 0x260); + Write(bytes, 56, 2); + Write(bytes, 0x298, 7); + Write(bytes, 0x29c, 4); + Write(bytes, 0x2a0, 0x248); + Write(bytes, 0x2a8, address); + Write(bytes, 0x2b0, address); + Write(bytes, 0x2b8, fileSize); + Write(bytes, 0x2c0, memorySize); + Write(bytes, 0x2c8, alignment); + return bytes; +} + void Require(const bool condition) { if (!condition) throw std::runtime_error("Unexpected parsed value"); } @@ -119,6 +136,31 @@ int main() { Write(bytes, 104, 0xd0); RejectProgramHeaders(std::move(bytes)); }}, + {"TLS image inside load memory and empty TLS placeholders", [] { + for (const auto& [address, fileSize, memorySize, alignment] : {std::tuple{0x1008, 8, 16, 8}, {0x1030, 16, 16, 16}, {0x9000, 0, 0, 0}, {0x9000, 0, 16, 16}}) { + const auto headers = Relinker::ElfReader(TlsFixture(address, fileSize, memorySize, alignment)).ReadProgramHeaders(); + Require(headers.size() == 2 && headers[1].Type == 7 && headers[1].MappedAddress == address); + } + }}, + {"TLS image outside load memory", [] { + constexpr std::uint64_t addresses[] = {0x0ff8, 0x103c, 0x2000, std::numeric_limits::max() - 3}; + for (const std::uint64_t address : addresses) { + RejectProgramHeaders(TlsFixture(address, 8, 8, 1)); + } + }}, + {"TLS file size exceeds memory size", [] { + RejectProgramHeaders(TlsFixture(0x1008, 16, 8, 8)); + }}, + {"TLS alignment is not a power of two", [] { + RejectProgramHeaders(TlsFixture(0x1008, 8, 8, 3)); + }}, + {"second TLS segment", [] { + auto bytes = TlsFixture(0x1008, 8, 16, 8); + bytes.resize(0x308); + std::memcpy(bytes.data() + 0x2d0, bytes.data() + 0x298, 56); + Write(bytes, 56, 3); + RejectProgramHeaders(std::move(bytes)); + }}, {"section entry size field", [] { const auto sections = Relinker::ElfReader(Fixture()).ReadSectionHeaders(); Require(sections.size() == 3 && sections[1].Name == ".text" && sections[1].EntrySize == 24); From 1876314b353c1595ab17713ac886c09a186d2902 Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Wed, 14 Oct 2026 23:40:30 +0530 Subject: [PATCH 058/131] docs: clarify macOS host support --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c63b83abe6..57c93abbf4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Tool for automatic executables porting to Linux and Windows. +The relinker-only build runs on macOS, including Apple silicon. Game execution on a macOS host is not yet supported. + [**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. ![anyps5_collage1](https://github.com/user-attachments/assets/ed2f0b2d-33d3-46d7-ab88-767ca872fc99) From 2b8e37ae0a3103900c8d0284c06bb84716d0452d Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 22:58:07 +0000 Subject: [PATCH 059/131] fix(libkernel): log resolved paths for failed opens --- core/libs/prx/libkernel/File/src/Open.cpp | 13 +++++-- core/libs/tests/GuestFilesystem.cpp | 42 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index c68f36535c..eb57b0ccf1 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -233,8 +233,17 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { } std::filesystem::path native; const bool module = (flags & (SCE_KERNEL_O_ACCMODE | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_RDONLY; - if (const int error = ResolvePathError_nid_no_patch(path, native, module, true)) return SceKernelError(error); - return OpenNative(native, flags, mode); + if (const int error = ResolvePathError_nid_no_patch(path, native, module, true)) { + const int result = SceKernelError(error); + APS5_LOG_ERR("open failed path=%s resolveError=%d result=0x%08X", path != nullptr ? path : "", error, static_cast(result)); + return result; + } + const int result = OpenNative(native, flags, mode); + if (result < 0) { + const auto resolved = native.u8string(); + APS5_LOG_ERR("open failed path=%s resolvedPath=%s result=0x%08X", path != nullptr ? path : "", reinterpret_cast(resolved.c_str()), static_cast(result)); + } + return result; } int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std::uint16_t mode) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b87cf1344c..c957d05d40 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -111,6 +112,40 @@ static bool ChflagsThrows(const std::filesystem::path& path, std::uint64_t flags return false; } +static std::string CaptureStderr(const std::function& action) { + std::FILE* capture = std::tmpfile(); + Require(capture != nullptr); + std::fflush(stderr); +#ifdef _WIN32 + const int stream = ::_fileno(stderr); + const int saved = ::_dup(stream); + Require(saved >= 0 && ::_dup2(::_fileno(capture), stream) == 0); +#else + const int stream = ::fileno(stderr); + const int saved = ::dup(stream); + Require(saved >= 0 && ::dup2(::fileno(capture), stream) >= 0); +#endif + action(); + std::fflush(stderr); +#ifdef _WIN32 + Require(::_dup2(saved, stream) == 0); + ::_close(saved); +#else + Require(::dup2(saved, stream) >= 0); + ::close(saved); +#endif + Require(std::fseek(capture, 0, SEEK_SET) == 0); + std::string output; + char buffer[512]; + for (;;) { + const auto bytes = std::fread(buffer, 1, sizeof(buffer), capture); + output.append(buffer, bytes); + if (bytes != sizeof(buffer)) break; + } + std::fclose(capture); + return output; +} + static void TestParentTraversal() { const auto root = std::filesystem::path("anyps5-parent-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -216,6 +251,13 @@ static void TestRelinkedModuleNames() { Require(sceKernelStat(original.c_str(), &status) == static_cast(0x80020002u)); Require(sceKernelCheckReachability(original.c_str()) == static_cast(0x80020002u)); Require(sceKernelOpen(original.c_str(), 0, 0) == static_cast(0x80020002u)); + const auto missing = (root / "missing-content.pak").string(); + int openResult = 0; + const auto openLog = CaptureStderr([&] { openResult = sceKernelOpen(missing.c_str(), 0, 0); }); + Require(openResult == static_cast(0x80020002u)); + Require(openLog.find("path=" + missing) != std::string::npos); + Require(openLog.find("resolvedPath=") != std::string::npos && openLog.find("missing-content.pak") != std::string::npos); + Require(openLog.find("result=0x80020002") != std::string::npos); { std::ofstream file(relinked); file << "relinked"; } Require(sceKernelStat(original.c_str(), &status) == 0 && status.st_size == 8); Require(stat_nid_postfix(original.c_str(), &status) == 0 && status.st_size == 8); From d33fdaa870198a8bad50118210442390191dcaeb Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Thu, 15 Oct 2026 18:03:39 +0300 Subject: [PATCH 060/131] test(agc): cover sampled multisample surface footprints --- .../prx/libSceAgcDriver/tests/GuestTextureResource.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index ad41779194..8043585e94 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -271,6 +271,13 @@ void RunGuestTextureResourceTests() { result = DecodeTextureResource(canvas); Require(result.dimension == TextureDimension::k2DMsaa && result.samples == 4 && result.baseAddress == 0x21f200000ull && result.width == 297 && result.height == 297 && result.format == 56 && result.tileMode == TextureTileMode::kR64KBX, "the 4x canvas descriptor of PPSA99006 decoded incorrectly"); Require(DescribeSurface(result).guestBytes == 0x190000, "4x render-target geometry must include fragment-aware padding for the PPSA99006 canvas"); + const auto canvasGeometry = DescribeSurface(result); + const std::array sonic{0x405fc080u, 0xc4700000u, 0x821bc3bfu, 0xe1b30facu, 0x00000000u, 0x00400030u, 0x406b0000u, 0x0040819fu}; + const auto sonicResource = DecodeTextureResource(sonic); + const auto sonicGeometry = DescribeSurface(sonicResource); + Require(sonicResource.width == 3840u && sonicResource.height == 2160u && sonicResource.samples == 8u && sonicResource.format == 71u, "the PPSA99902 multisampled color descriptor decoded incorrectly"); + Require(sonicGeometry.guestBytes == 0x1fe00000ull, "the PPSA99902 8x surface footprint omits fragments: " + std::to_string(sonicGeometry.guestBytes)); + Require(canvasGeometry.guestBytes == 0x190000u, "the 297x297 4x surface footprint does not use fragment-aware blocks"); Fields fewerFragments = multisampled; fewerFragments.maxMip = 1; rejectFields(fewerFragments, "same sample count"); From 54d941941ca9428ac8fe15c98b4160f99565a51f Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Fri, 16 Oct 2026 23:32:07 +0300 Subject: [PATCH 061/131] test(agc): cover MSAA geometry metadata and linear descriptors --- .../tests/GuestTextureResource.cpp | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp index 8043585e94..3b88132328 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp @@ -3,6 +3,7 @@ #include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp" +#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp" #include #include #include @@ -278,6 +279,81 @@ void RunGuestTextureResourceTests() { Require(sonicResource.width == 3840u && sonicResource.height == 2160u && sonicResource.samples == 8u && sonicResource.format == 71u, "the PPSA99902 multisampled color descriptor decoded incorrectly"); Require(sonicGeometry.guestBytes == 0x1fe00000ull, "the PPSA99902 8x surface footprint omits fragments: " + std::to_string(sonicGeometry.guestBytes)); Require(canvasGeometry.guestBytes == 0x190000u, "the 297x297 4x surface footprint does not use fragment-aware blocks"); + auto singleSample = sonicResource; + singleSample.samples = 1u; + singleSample.dimension = TextureDimension::k2D; + Require(DescribeSurface(singleSample).guestBytes == 0x3fc0000u, "single-sample surface geometry changed"); + struct FootprintCase { + TextureTileMode tile; + VkFormat format; + std::uint32_t elementBytes, width, height, samples, blocksPerRow; + std::uint64_t bytes; + }; + const std::array footprints{ + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R8_UINT, 1u, 129u, 65u, 2u, 2u, 0x20000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R16_UINT, 2u, 65u, 129u, 4u, 1u, 0x30000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 129u, 65u, 2u, 3u, 0x30000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 65u, 129u, 2u, 2u, 0x40000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 297u, 297u, 4u, 5u, 0x190000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32_UINT, 4u, 65u, 129u, 8u, 3u, 0x90000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32G32_SFLOAT, 8u, 3840u, 2160u, 8u, 120u, 0x1fe00000u}, + FootprintCase{TextureTileMode::kR64KBX, VK_FORMAT_R32G32B32A32_UINT, 16u, 16384u, 16384u, 8u, 1024u, 0x800000000ull}, + FootprintCase{TextureTileMode::kZ64KBX, VK_FORMAT_R16_UNORM, 2u, 129u, 65u, 2u, 2u, 0x20000u}, + FootprintCase{TextureTileMode::kZ64KBX, VK_FORMAT_R32_SFLOAT, 4u, 65u, 129u, 8u, 3u, 0x90000u}, + FootprintCase{TextureTileMode::kStandard4KB, VK_FORMAT_R32_UINT, 4u, 17u, 33u, 4u, 2u, 0x6000u}, + FootprintCase{TextureTileMode::kStandard64KB, VK_FORMAT_R32_UINT, 4u, 129u, 65u, 2u, 3u, 0x30000u} + }; + for (const auto& test : footprints) { + auto resource = sonicResource; + const auto format = FindGuestTextureFormat(test.format, test.elementBytes); + Require(format.has_value(), "the multisample footprint test format has no guest encoding"); + resource.format = format.value(); + resource.width = test.width; + resource.height = test.height; + resource.samples = test.samples; + resource.tileMode = test.tile; + const auto geometry = DescribeSurface(resource); + const auto linearBytes = static_cast(test.width) * test.height * test.samples * test.elementBytes; + Require(geometry.guestBytes == test.bytes && geometry.layerBytes == test.bytes && geometry.sliceLinearBytes == linearBytes, "multisampled surface footprint or linear size disagrees with the reference"); + Require(geometry.layers == 1u && geometry.imageLayers == 1u && geometry.imageDepth == 1u && !geometry.thick && geometry.blockDepth == 1u && geometry.mips.size() == 1u, "multisampled surface geometry is not one thin 2D level"); + const auto& mip = geometry.mips.front(); + Require(mip.tiledOffset == 0u && mip.tiledSize == test.bytes && mip.linearOffset == 0u && mip.linearSize == linearBytes && mip.width == test.width && mip.height == test.height && mip.blocksPerRow == test.blocksPerRow && mip.pitchBytes == test.width * test.samples * test.elementBytes && !mip.tail && mip.tailX == 0u && mip.tailY == 0u, "multisampled mip geometry lost its sample pitch or block layout"); + } + auto unsupportedSurface = sonicResource; + for (const auto samples : {0u, 3u, 16u}) { + unsupportedSurface.samples = samples; + reject([&] { DescribeSurface(unsupportedSurface); }, "fragment count"); + } + unsupportedSurface = sonicResource; + unsupportedSurface.mipCount = 2u; + reject([&] { DescribeSurface(unsupportedSurface); }, "mip levels"); + unsupportedSurface = sonicResource; + unsupportedSurface.dimension = TextureDimension::k2DArray; + reject([&] { DescribeSurface(unsupportedSurface); }, "dimension or array"); + unsupportedSurface = sonicResource; + unsupportedSurface.tileMode = TextureTileMode::kStandard256B; + reject([&] { DescribeSurface(unsupportedSurface); }, "4 KiB or 64 KiB"); + for (const auto format : {56u, 71u}) { + for (const auto samples : {2u, 4u, 8u}) { + for (const auto extent : {std::array{1u, 1u}, std::array{17u, 5u}, std::array{297u, 297u}}) { + auto linear = sonicResource; + linear.tileMode = TextureTileMode::kLinear; + linear.format = format; + linear.samples = samples; + linear.width = extent[0]; + linear.height = extent[1]; + auto single = linear; + single.samples = 1u; + single.dimension = TextureDimension::k2D; + const auto expected = DescribeSurface(single); + const auto actual = DescribeSurface(linear); + Require(actual.guestBytes == expected.guestBytes && actual.layerBytes == expected.layerBytes && actual.sliceLinearBytes == expected.sliceLinearBytes && actual.mips.size() == expected.mips.size(), "linear multisampled descriptors must retain the existing geometry path"); + const auto& mip = actual.mips.front(); + const auto& baseline = expected.mips.front(); + Require(mip.tiledSize == baseline.tiledSize && mip.linearSize == baseline.linearSize && mip.pitchBytes == baseline.pitchBytes && mip.blocksPerRow == baseline.blocksPerRow, "linear multisampled descriptors changed existing mip geometry"); + } + } + } Fields fewerFragments = multisampled; fewerFragments.maxMip = 1; rejectFields(fewerFragments, "same sample count"); From b4e66bfde8e4bc397cf21fca585f47468abee893 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Fri, 16 Oct 2026 23:33:49 +0300 Subject: [PATCH 062/131] fix(agc): complete sampled MSAA geometry after fragment sizing --- .../libSceAgcDriver/Graphics/src/TextureTiling.cpp | 12 +++++++++++- docs/dev/debt/libSceAgcDriver.md | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp index 24429b8a72..f819d7fbba 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp @@ -382,6 +382,13 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { SurfaceGeometry geometry; + if (descriptor.samples != 1u && descriptor.tileMode != TextureTileMode::kLinear) { + Require(descriptor.samples == 2u || descriptor.samples == 4u || descriptor.samples == 8u, "multisampled surface fragment count is not implemented"); + Require(descriptor.dimension == TextureDimension::k2DMsaa && descriptor.baseArray == 0u && descriptor.depthOrLastArray == 0u, "multisampled surface dimension or array range is not implemented"); + Require(descriptor.mipCount == 1u && descriptor.allocatedMipCount == 1u && descriptor.baseLevel == 0u && descriptor.lastLevel == 0u, "multisampled surface mip levels are not implemented"); + Require(descriptor.width != 0u && descriptor.width <= 16384u && descriptor.height != 0u && descriptor.height <= 16384u, "invalid multisampled surface extent"); + Require(!IsBlockCompressed(descriptor.format), "multisampled block-compressed surface is not implemented"); + } if (descriptor.dimension == TextureDimension::k3D && (descriptor.tileMode == TextureTileMode::kZ64KBX || descriptor.tileMode == TextureTileMode::kR64KBX)) { const auto depth = descriptor.depthOrLastArray + 1u; geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount); @@ -405,11 +412,12 @@ SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { geometry.layerBytes = thick.slabBytes; return geometry; } - if (descriptor.dimension == TextureDimension::k2DMsaa) { + if (descriptor.dimension == TextureDimension::k2DMsaa && descriptor.tileMode != TextureTileMode::kLinear) { Require(descriptor.samples > 1u && std::has_single_bit(descriptor.samples), "multisampled texture has an invalid sample count"); const auto bytesPerElement = BytesPerElement(descriptor.format); Require(std::has_single_bit(bytesPerElement), "multisampled texture format has an unsupported element size"); const auto block = GetBlockLayout(descriptor.tileMode, bytesPerElement); + Require(block.blockSize == 4096u || block.blockSize == 65536u, "multisampled surface requires 4 KiB or 64 KiB blocks"); const auto log2Samples = static_cast(std::countr_zero(descriptor.samples)); const auto log2Pixels = static_cast(std::countr_zero(block.blockSize)) - static_cast(std::countr_zero(bytesPerElement)) - log2Samples; const auto log2Width = (log2Pixels + ((log2Samples & 1u) == 0u ? 1u : 0u)) / 2u; @@ -423,6 +431,8 @@ SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor) { geometry.layers = 1; geometry.imageLayers = 1; geometry.imageDepth = 1; + geometry.sliceLinearBytes = static_cast(descriptor.width) * descriptor.height * descriptor.samples * bytesPerElement; + geometry.mips.push_back({0u, geometry.guestBytes, 0u, geometry.sliceLinearBytes, descriptor.width, descriptor.height, static_cast(align(descriptor.width, blockWidth) / blockWidth), descriptor.width * descriptor.samples * bytesPerElement, false, 0u, 0u}); return geometry; } geometry.mips = ComputeMipLayout(descriptor.tileMode, descriptor.format, descriptor.width, descriptor.height, descriptor.mipCount); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index ee8be5c195..cc79c2d6d3 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -43,6 +43,8 @@ ### Functional +- [Sampled MSAA surface geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) supplies the fragment-aware linear size and one mip for a tiled 2D surface at two, four or eight fragments. Unsupported arrays, mip chains, extents and block sizes throw. Linear descriptors retain the existing geometry path, including its use when sampling an existing native target; a linear MSAA guest layout is not modeled. The range calculation follows AMD Addrlib's thin-block dimensions. Console layout equivalence and title effects of the additional mip metadata remain unmeasured. This does not add sample upload or readback. + - [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. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. From 3a9240a50d8012644b20c83830b4dafb2f82d16b Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Fri, 16 Oct 2026 23:44:27 +0300 Subject: [PATCH 063/131] docs(debt): avoid adjacent MSAA contribution insertions --- docs/dev/debt/libSceAgcDriver.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index cc79c2d6d3..878cb65024 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -43,8 +43,6 @@ ### Functional -- [Sampled MSAA surface geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) supplies the fragment-aware linear size and one mip for a tiled 2D surface at two, four or eight fragments. Unsupported arrays, mip chains, extents and block sizes throw. Linear descriptors retain the existing geometry path, including its use when sampling an existing native target; a linear MSAA guest layout is not modeled. The range calculation follows AMD Addrlib's thin-block dimensions. Console layout equivalence and title effects of the additional mip metadata remain unmeasured. This does not add sample upload or readback. - - [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. Retired Vulkan images remain allocated until device teardown to preserve outstanding references. Partial unmaps keep the guard; partial allocation reuse and guest access to live multisampled data remain unsupported. @@ -164,3 +162,5 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. + +- [Sampled MSAA surface geometry](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) supplies the fragment-aware linear size and one mip for a tiled 2D surface at two, four or eight fragments. Unsupported arrays, mip chains, extents and block sizes throw. Linear descriptors retain the existing geometry path, including its use when sampling an existing native target; a linear MSAA guest layout is not modeled. The range calculation follows AMD Addrlib's thin-block dimensions. Console layout equivalence and title effects of the additional mip metadata remain unmeasured. This does not add sample upload or readback. From 4a2b7d34f2b0f3865dd588a5d0ed1877b843d7e3 Mon Sep 17 00:00:00 2001 From: ivica00 Date: Fri, 16 Oct 2026 23:03:00 +0200 Subject: [PATCH 064/131] perf(agc): pipeline DMA_DATA by default without losing earlier writes A copy reads its source when the pipeline runs it, so it sees the fill ahead of it. Large immediate fills stay buffer fills, host copies stay counted, and marker push and pop still update the queue. --- .../src/Driver/Packets/PacketExecution.cpp | 13 ++- .../src/Driver/Packets/PacketMemory.cpp | 18 +++- .../tests/execution/PipelinedDma.cpp | 102 +++++++++++++++++- docs/dev/ARCHITECTURE.md | 2 +- 4 files changed, 129 insertions(+), 6 deletions(-) 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 02458b6484..bd6f0050da 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -27,14 +27,23 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; if (Pm4::DrawOpcode(opcode)) return true; switch (opcode) { - case 0x11: case 0x12: case 0x13: case 0x26: case 0x2a: case 0x2f: case 0x42: case 0x58: case 0x59: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: return true; + case 0x11: case 0x12: case 0x13: case 0x20: case 0x26: case 0x2a: case 0x2f: case 0x42: case 0x58: case 0x59: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: return true; case 0x46: return packet.size() >= 2 && (packet[1] & 0x3fu) != 0x39u; case 0x15: return orderedFills(); case 0x63: case 0x64: case 0x9f: return packet.size() >= 5 && !DrawPipeline::Queue0().Overlaps(packet[1] | (static_cast(packet[2]) << 32u), static_cast(packet[4]) * 8); + case 0x50: { + if (packet.size() < 7) return false; + const auto destination = Pm4::DmaDestination(packet); + if (destination == Pm4::DmaSelectGds || destination == Pm4::DmaDestinationNowhere) return true; + const auto bytes = Pm4::DmaBytes(packet); + if (bytes == 0) return true; + const auto address = packet[4] | (static_cast(packet[5]) << 32u); + return !DrawPipeline::Queue0().Overlaps(address, bytes); + } case 0x10: { const auto operation = (header >> 2u) & 0x3fu; - return operation == 0 || operation == 0x09 || operation == 0x0b || operation == 0x0c || operation == 0x1a; + return operation != 0x09; } default: return false; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 452c674ab3..a7d985279d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -268,10 +268,25 @@ void Driver::dumpSampleCounters(std::uint64_t address) { } bool Driver::enqueueDmaPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state) { - static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; + static const bool enabled = std::getenv("APS5_NO_PIPELINE_DMA") == nullptr; if (!enabled || opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false; const auto source = ((packet[1] >> 29u) & 3u) | ((packet[6] >> 24u) & 4u) | ((packet[6] >> 25u) & 8u); constexpr std::size_t immediateLimit = std::size_t{16} << 20u; + constexpr std::size_t gpuStoreLimit = 65536; + if (source == 2 && Pm4::DmaBytes(packet) > gpuStoreLimit && Pm4::DmaDestination(packet) != Pm4::DmaSelectGds) { + const auto fillAddress = packet[4] | (static_cast(packet[5]) << 32u); + const auto fillBytes = Pm4::DmaBytes(packet); + const auto pattern = packet[2]; + if (fillAddress % 16 != 0 || fillBytes % 16 != 0 || !GuestMemory::Accessible(reinterpret_cast(fillAddress), fillBytes, true) || !device.Load()) return false; + DrawPipeline::Queue0().Enqueue([this, queue, fillAddress, fillBytes, pattern] { + GuestMemory::SetCurrentPacket(0x50, queue); + if (const auto localDevice = device.Load()) { + storeBufferFill(queue, fillAddress, fillBytes, {pattern, pattern, pattern, pattern}, localDevice, false, false); + ++dmaFills; + } + }, {{fillAddress, fillAddress + fillBytes}}); + return true; + } if (source == 2) { const auto store = Pm4::ResolveStore(packet, state, immediateLimit); if (!store.has_value() || store->Bytes().empty() || !GuestMemory::Accessible(reinterpret_cast(store->address), store->Bytes().size(), true)) return false; @@ -305,6 +320,7 @@ bool Driver::enqueueDmaPacket(std::span packet, std::uint32 recordDeferredLabels(localDevice.get(), queue); const auto outcome = localDevice->CopyBuffer(copy.destination, copy.source, copy.bytes, 0, std::numeric_limits::max(), 0, 0, queue, [](std::span, std::uint64_t) {}); if (outcome.path == 1 || outcome.path == 3) return; + if (outcome.path == 2 && !Graphics::RegisteredReadableCovers(copy.destination, copy.bytes) && !localDevice->StoresPendingOver(copy.destination, copy.bytes)) ++copiesToHostMemory; } } std::vector bytes(copy.bytes); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp index c470b4fdc0..71a77737d5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp @@ -1,8 +1,11 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/Shutdown.hpp" #include +#include +#include #include #include #include @@ -10,10 +13,13 @@ #include #include #include +#include #include namespace { +using AgcDriver::DriverDetail::DrawPipeline; + void check(bool condition, const std::string& what) { if (!condition) throw std::runtime_error("pipelined DMA: " + what); } @@ -22,26 +28,66 @@ std::array DmaData(bool immediate, std::uint64_t source, std:: return {0xc0055000u, immediate ? (2u << 29u) : 0u, immediate ? data : static_cast(source), immediate ? 0u : static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), bytes}; } +std::array DmaSelect(std::uint32_t destinationSelect, std::uint64_t source, std::uint64_t destination, std::uint32_t bytes) { + return {0xc0055000u, destinationSelect << 20u, static_cast(source), static_cast(source >> 32u), static_cast(destination), static_cast(destination >> 32u), bytes}; +} + +template +void Append(std::vector& words, const std::array& packet) { + words.insert(words.end(), packet.begin(), packet.end()); +} + void Submit(std::vector& words) { Packet packet{words.data(), static_cast(words.size()), 0, {}}; check(sceAgcDriverSubmitDcb(&packet) == 0, "submission failed"); } +struct Gate { + std::atomic release{false}; + ~Gate() { release.store(true, std::memory_order_release); } +}; + +void Hold(Gate& gate, std::uint64_t address, std::size_t bytes) { + DrawPipeline::Queue0().Enqueue([&gate] { + while (!gate.release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + }, {{address, address + bytes}}); +} + +void ExpectDrains(std::vector& words, std::uint32_t opcode, Gate& gate, const char* what) { + const auto before = DrawPipeline::DrainRequestsByOpcode(opcode); + std::thread releaser([&] { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (DrawPipeline::DrainRequestsByOpcode(opcode) == before && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + gate.release.store(true, std::memory_order_release); + }); + Submit(words); + releaser.join(); + check(DrawPipeline::DrainRequestsByOpcode(opcode) == before + 1, what); +} + +void ExpectNoDrain(std::uint32_t opcode, std::uint64_t before, const char* what) { + check(DrawPipeline::DrainRequestsByOpcode(opcode) == before, what); +} + } int main() { #ifdef _WIN32 _putenv_s("APS5_PIPELINED_DRAWS", "1"); - _putenv_s("APS5_PIPELINE_DMA", "1"); + _putenv_s("APS5_NO_PIPELINE_DMA", ""); #else setenv("APS5_PIPELINED_DRAWS", "1", 1); - setenv("APS5_PIPELINE_DMA", "1", 1); + unsetenv("APS5_NO_PIPELINE_DMA"); #endif try { alignas(256) static std::array first{}; alignas(256) static std::array second{}; + alignas(256) static std::array pending{}; + alignas(256) static std::array elsewhere{}; first.fill(0); second.fill(0); + pending.fill(0); + elsewhere.fill(0); using namespace AgcDriver::DriverDetail; const auto drainedBefore = storesDrained.load(); const auto cpuBefore = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load(); @@ -57,6 +103,58 @@ int main() { const auto stores = storesOnCpu.load() + storesOnGpu.load() + storesBehindCompletions.load() - cpuBefore; check(stores == 2, "the DMA_DATA packets were not committed (" + std::to_string(stores) + " committed stores)"); check(storesDrained.load() == drainedBefore, "a DMA_DATA packet still drained"); + + const auto source = reinterpret_cast(first.data()); + const auto held = reinterpret_cast(pending.data()); + const auto other = reinterpret_cast(elsewhere.data()); + { + Gate gate; + Hold(gate, held, sizeof(pending)); + const auto dmaBefore = DrawPipeline::DrainRequestsByOpcode(0x50); + const auto predicationBefore = DrawPipeline::DrainRequestsByOpcode(0x20); + const auto nopBefore = DrawPipeline::DrainRequestsByOpcode(0x10); + words.clear(); + Append(words, DmaSelect(2, source, 0, 64)); + Append(words, DmaData(false, source, 0, other, 0)); + Append(words, DmaSelect(1, source, 0, 4)); + Append(words, DmaData(false, source, 0, other, 2)); + Append(words, std::array{0xc0022000u, 0x31100u, static_cast(held), static_cast(held >> 32u)}); + Append(words, std::array{0xc000102cu, 0x00636261u}); + Append(words, std::array{0xc0001030u, 0u}); + Submit(words); + ExpectNoDrain(0x50, dmaBefore, "a prefetch, a zero-byte copy, a GDS copy or an unaligned copy off the pending range drained"); + ExpectNoDrain(0x20, predicationBefore, "SET_PREDICATION drained the pipeline"); + ExpectNoDrain(0x10, nopBefore, "a marker push or pop drained the pipeline"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + words.clear(); + Append(words, DmaData(false, source, 0, held, 2)); + ExpectDrains(words, 0x50, gate, "a copy onto an uncommitted pipeline write did not drain"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + words.clear(); + Append(words, std::array{0xc0022000u, 0x31100u, static_cast(held), static_cast(held >> 32u)}); + Append(words, std::array{0xc0001301u, 32u}); + ExpectDrains(words, 0x13, gate, "a predicated packet whose predicate overlaps an uncommitted write did not drain"); + } + AgcDriverWaitIdle_nid_postfix(); + + { + Gate gate; + Hold(gate, held, sizeof(pending)); + words.clear(); + Append(words, std::array{0xc0001024u, 0u}); + ExpectDrains(words, 0x10, gate, "DISPATCH_RESET did not drain the pipeline"); + } + LibcRunShutdown_nid_postfix(); std::puts("pipelined DMA tests passed"); return 0; diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 3833a11d11..35642e7b88 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -98,7 +98,7 @@ flowchart LR - The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the first `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (1024, the pipeline store's bound, `APS5_PIPELINE_STORE_ENTRIES`; a run reaches those first), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). -- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. +- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and DMA packets are queued as items instead (`APS5_NO_PIPELINE_DMA` leaves a DMA packet on the worker). With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - 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. From 56b43aae5aa6a1ab27493050f3262d5896998979 Mon Sep 17 00:00:00 2001 From: Zinebyte <197974009+Zinebyte@users.noreply.github.com> Date: Wed, 14 Oct 2026 18:15:15 -0400 Subject: [PATCH 065/131] fix(relinker): scope host dependencies to required guest modules --- .../relinker/src/guest/GuestModuleBuilder.cpp | 29 ++++--- .../relinker/tests/test_guest_module_args.py | 55 ++++++++++++ .../relinker/tests/test_guest_module_dir.py | 85 ++++++++++++++++++- .../tests/test_guest_module_identity.py | 8 +- 4 files changed, 161 insertions(+), 16 deletions(-) diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index 33cc5bdfc2..495ba9038d 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -292,14 +292,27 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path }; for (std::size_t index = 0; index < images.size(); ++index) visit(index); std::vector hostLibraries; + std::vector> moduleHostLibraries(images.size()); std::set uniqueHosts; - const auto addHost = [&](const std::string& name) { + const auto addHost = [&](const std::string& name, const bool startup) { if (findGuest(name) != guestNames.end()) return; if (name.empty() || name.find_first_of("/\\:$") != std::string::npos) throw Domain::RelinkerException("Invalid host dependency: " + name); - if (uniqueHosts.insert(name).second) hostLibraries.push_back(name); + if (startup && uniqueHosts.insert(name).second) hostLibraries.push_back(name); }; - for (const auto& name : neededNames) addHost(name); - for (const auto& image : images) for (const auto& dependency : image.Dependencies) addHost(dependency); + for (const auto& name : neededNames) addHost(name, true); + for (std::size_t index = 0; index < images.size(); ++index) { + auto& hosts = moduleHostLibraries[index]; + for (const auto& dependency : images[index].Dependencies) { + addHost(dependency, windows || !deferred(index)); + if (findGuest(dependency) == guestNames.end()) hosts.push_back(dependency); + } + if (!windows && !macos && images[index].Init != 0) { + if (std::find(hosts.begin(), hosts.end(), "libkernel.prx") == hosts.end()) hosts.push_back("libkernel.prx"); + if (!deferred(index) && uniqueHosts.insert("libkernel.prx").second) hostLibraries.push_back("libkernel.prx"); + } + if (std::find(hosts.begin(), hosts.end(), "libSceLibcInternal.prx") != hosts.end() && + std::find(hosts.begin(), hosts.end(), "libc.prx") == hosts.end()) hosts.push_back("libc.prx"); + } if (uniqueHosts.contains("libSceLibcInternal.prx") && uniqueHosts.insert("libc.prx").second) hostLibraries.push_back("libc.prx"); dynamic.NeededLibraries = hostLibraries; dynamic.DynamicSegmentData.clear(); @@ -313,12 +326,6 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!windows && runPath != "$ORIGIN" && !runPath.starts_with("$ORIGIN/") && !runPath.starts_with("$ORIGIN:") && !runPath.starts_with('/')) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN"); const auto outputDirectory = outputPath.parent_path().lexically_normal(); std::vector artifacts; - std::vector moduleHostLibraries; - std::set uniqueModuleHosts; - for (const auto& image : images) for (const auto& dependency : image.Dependencies) { - if (findGuest(dependency) == guestNames.end() && uniqueModuleHosts.insert(dependency).second) moduleHostLibraries.push_back(dependency); - } - if (uniqueModuleHosts.contains("libSceLibcInternal.prx") && uniqueModuleHosts.insert("libc.prx").second) moduleHostLibraries.push_back("libc.prx"); for (const auto index : order) { auto& image = images[index]; progress.Report({Domain::ProgressStage::Modules, Io::Utf8Path(image.SourcePath), artifacts.size(), order.size()}); @@ -363,7 +370,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path const auto dependencyPath = images[dependency].SourcePath.parent_path() / images[dependency].OutputName; needed.push_back("$ORIGIN/" + dependencyPath.lexically_relative(image.SourcePath.parent_path()).generic_string()); } - needed.insert(needed.end(), moduleHostLibraries.begin(), moduleHostLibraries.end()); + needed.insert(needed.end(), moduleHostLibraries[index].begin(), moduleHostLibraries[index].end()); output = macos ? Elfpatcher::GuestModuleWriter().WriteMacOs(image, needed, guestRunPath) : Elfpatcher::GuestModuleWriter().WriteLinux(image, needed, guestRunPath); } if (windows) { diff --git a/core/relinker/relinker/tests/test_guest_module_args.py b/core/relinker/relinker/tests/test_guest_module_args.py index 6c3de5f3a4..5aee388957 100644 --- a/core/relinker/relinker/tests/test_guest_module_args.py +++ b/core/relinker/relinker/tests/test_guest_module_args.py @@ -10,6 +10,9 @@ import tempfile from test_guest_symbol_names import dynamic_tags, imported_symbols, module_symbols from test_macos_output import MachO from test_windows_import_modules import executable +from test_guest_intel_trampolines import main_fixture +from test_guest_module_directories import needed_libraries +from test_guest_needed_modules import module_with_needed ARGS = 0x1122334455667788 ARGP = 0x99AABBCCDDEEFF00 @@ -56,6 +59,55 @@ def no_relocation_module(): return image +def check_lifecycle_host(relinker, host, work, linux, deferred): + case = work / f'lifecycle-host-{deferred}' + modules, plugins = case / 'sce_module', case / 'Media/Plugins' + modules.mkdir(parents=True) + plugins.mkdir(parents=True) + image = no_relocation_module() + image[0x1010:0x1013] = b'\x31\xc0\xc3' + tags = [] + for position in range(0x2000, 0x2200, 16): + tag, value = struct.unpack_from(' Date: Fri, 16 Oct 2026 17:56:47 -0400 Subject: [PATCH 066/131] test(relinker): provide kernel fixture for guest finalizers --- core/relinker/relinker/tests/test_guest_module_fini.py | 1 + 1 file changed, 1 insertion(+) diff --git a/core/relinker/relinker/tests/test_guest_module_fini.py b/core/relinker/relinker/tests/test_guest_module_fini.py index aa79a1fc69..afbd17bc93 100644 --- a/core/relinker/relinker/tests/test_guest_module_fini.py +++ b/core/relinker/relinker/tests/test_guest_module_fini.py @@ -66,6 +66,7 @@ def main(): libraries.mkdir() provider = libraries / 'libc.prx' shutil.copyfile(host, provider) + shutil.copyfile(host, libraries / 'libkernel.prx') events = work / 'events.txt' run = subprocess.run([sys.executable, __file__, '--load', str(provider), str(converted)], env={**os.environ, 'ANYPS5_GUEST_MODULE_ARGS': str(events)}, From 890c3218420b9d103fa3f2eaa457788ce6cb3e23 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 14:18:25 -0700 Subject: [PATCH 067/131] fix(agc): give the overlay's view the transfer usage of its pass layouts The overlay's render pass starts in the layout the image was left in and ends in the one its caller needs next, but the attachment view allowed only COLOR_ATTACHMENT usage. A pass that starts in TRANSFER_DST_OPTIMAL (the present path when nothing was drawn, and the tests) or ends in TRANSFER_SRC_OPTIMAL (the tests' readback) then broke VUID-vkCmdBeginRenderPass-initialLayout-00898 and -00899. The view now also allows TRANSFER_DST or TRANSFER_SRC when the pass names that layout. Swapchain images have TRANSFER_DST and their pass ends in PRESENT_SRC, so the present path asks only for that; the test's cost report image, whose pass ends in TRANSFER_SRC_OPTIMAL, is now created with TRANSFER_SRC. --- .../prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp | 5 +++++ core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp index d7b2b9aebf..e21ddf0599 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -743,6 +744,10 @@ OverlayRect PerformanceOverlay::Record(VkCommandBuffer commands, std::size_t ind viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; VkImageViewUsageCreateInfo usageInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO}; usageInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + for (const auto layout : {initialLayout, finalLayout}) { + if (layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) usageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL) usageInfo.usage |= VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + } viewInfo.pNext = &usageInfo; VkImageView view = VK_NULL_HANDLE; Graphics::Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView performance overlay"); diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index ceb94348e8..860f34e2c3 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -575,7 +575,7 @@ void costReport(const Device& device, PerformanceOverlay& overlay) { const auto vramUs = std::chrono::duration(std::chrono::steady_clock::now() - sampled).count() / 100; std::printf("VRAM budget read in %.1f us (%s), once a second while the overlay is shown\n", vramUs, device.MemoryBudget() ? "VK_EXT_memory_budget" : "no VK_EXT_memory_budget"); for (const VkExtent2D extent : {VkExtent2D{1920, 1080}, VkExtent2D{3840, 2160}}) { - Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT); + Image destination(context, VK_FORMAT_B8G8R8A8_UNORM, extent, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT); const auto content = fullPanel(fixedDetails(extent, extent)); std::vector gpuMs; std::vector cpuMs; From 84cae0bda0fdd7744ed781af9cee079c74c4b4d6 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:45:52 -0700 Subject: [PATCH 068/131] fix(agc): pick the overlay's palette entry with constant indices On RADV the overlay's fragment shader read parameters.palette[paletteIndex] as palette[0] for the text and graph rectangles, so they were drawn in the panel's colour (0,0,0,0.6) and darkened the panel a second time instead of drawing white or green. On an AMD Radeon RX 7600 (NAVI33) with Mesa 26.0.8, agc_performance_overlay failed in its SDR case at "'6' of 'FPS: 60.0' glyph pixel 2,0 is not white text: 0x4280161290": 0xff1e140a is the clear colour (191,128,64) darkened by 0.4 twice, where the panel alone gives 0xff4c331a. An integrated Radeon 660M failed at the full panel's first glyph. The shader was valid SPIR-V: a flat uint input indexing a push-constant array (the palette at offset 0, the extent at 48). The index is per rectangle, and fragments of the panel and of its glyphs share waves, so it varies within a wave; RADV read that divergent index into the push-constant array as 0. The shader now reads each entry at a constant index and selects one by the colour index. Every case passes on that RX 7600 with this change, full panel included, and still passes on llvmpipe from the same Mesa. --- .../libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag index 33f1f9740d..435fb75d5a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag +++ b/core/libs/prx/libSceAgcDriver/Graphics/shaders/PerformanceOverlay.frag @@ -8,5 +8,5 @@ layout(location = 0) flat in uint paletteIndex; layout(location = 0) out vec4 color; void main() { - color = parameters.palette[paletteIndex]; + color = paletteIndex == 1u ? parameters.palette[1] : paletteIndex == 2u ? parameters.palette[2] : parameters.palette[0]; } From 487b9fd4e4885867eca402dc001ffeb1ea4c717c Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sun, 11 Oct 2026 17:46:31 +0300 Subject: [PATCH 069/131] feat(libkernel): read param.sfo when param.json is absent PS4 titles ship sce_sys/param.sfo instead of param.json. When param.json is absent, the application metadata now reads the title, title ID, download data size and user defined parameters from param.sfo, and /data and /download0 are mounted for it as for param.json. param.json is still preferred when both files exist. --- .../AppMetadata/include/ParamSfoParser.hpp | 10 ++ .../libkernel/AppMetadata/src/AppMetadata.cpp | 20 ++- .../AppMetadata/src/ParamSfoParser.cpp | 85 +++++++++++ core/libs/prx/libkernel/CMakeLists.txt | 14 +- core/libs/tests/GuestAppInfo.cpp | 47 +++++- core/libs/tests/GuestDownloadDataMount.cpp | 32 +++- core/libs/tests/ParamSfoParser.cpp | 144 ++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 8 files changed, 334 insertions(+), 19 deletions(-) create mode 100644 core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp create mode 100644 core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp create mode 100644 core/libs/tests/ParamSfoParser.cpp diff --git a/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp b/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp new file mode 100644 index 0000000000..4e828a4970 --- /dev/null +++ b/core/libs/prx/libkernel/AppMetadata/include/ParamSfoParser.hpp @@ -0,0 +1,10 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMSFOPARSER_HPP +#define CORE_LIBS_PRX_LIBKERNEL_APPMETADATA_INCLUDE_PARAMSFOPARSER_HPP + +#include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" + +#include + +ParsedParamJson parseParamSfo(const std::filesystem::path& paramSfoPath); + +#endif diff --git a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp index c2c9b2d2c6..f078143320 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/AppMetadata.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/AppMetadata/include/ParamJsonParser.hpp" +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" #include "prx/libc/include/General.hpp" #include @@ -25,6 +26,7 @@ std::int32_t g_userDefinedParams[4] = {}; std::vector g_iconBytes; constexpr const char* AppMetadataParamJsonGuestPath = "/app0/sce_sys/param.json"; +constexpr const char* AppMetadataParamSfoGuestPath = "/app0/sce_sys/param.sfo"; constexpr const char* AppMetadataIconGuestPath = "/app0/sce_sys/icon0.png"; void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std::string& source) { @@ -33,12 +35,16 @@ void copyToFixedBuffer(char* destination, std::size_t destinationSize, const std std::memcpy(destination, source.c_str(), source.size() + 1); } +ParsedParamJson parseParams(const std::filesystem::path& paramJson, const std::filesystem::path& paramSfo) { + if (std::filesystem::exists(paramJson)) return parseParamJson(paramJson); + if (std::filesystem::exists(paramSfo)) return parseParamSfo(paramSfo); + throw std::runtime_error("param.json or param.sfo not found"); +} + void ensureTitleLoaded() { std::lock_guard lock(g_metadataMutex); if (g_titleLoaded) return; - const auto resolvedPath = ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath); - if (!std::filesystem::exists(resolvedPath)) throw std::runtime_error("param.json not found"); - const auto parsed = parseParamJson(resolvedPath); + const auto parsed = parseParams(ResolvePath_nid_no_patch(AppMetadataParamJsonGuestPath), ResolvePath_nid_no_patch(AppMetadataParamSfoGuestPath)); copyToFixedBuffer(g_title, sizeof(g_title), parsed.title); copyToFixedBuffer(g_titleId, sizeof(g_titleId), parsed.titleId); g_downloadDataSizeMiB = parsed.downloadDataSizeMiB; @@ -50,8 +56,9 @@ void ensureTitleLoaded() { const bool g_downloadDataMounted = [] { try { const auto run = std::filesystem::current_path(); - const auto paramJson = run / "app0" / "sce_sys" / "param.json"; - if (!std::filesystem::exists(paramJson) || parseParamJson(paramJson).downloadDataSizeMiB == 0) return false; + const auto metadata = run / "app0" / "sce_sys"; + if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false; + if (parseParams(metadata / "param.json", metadata / "param.sfo").downloadDataSizeMiB == 0) return false; std::filesystem::create_directories(run / "download0"); return true; } catch (const std::exception&) { @@ -62,7 +69,8 @@ const bool g_downloadDataMounted = [] { const bool g_dataMounted = [] { try { const auto run = std::filesystem::current_path(); - if (!std::filesystem::exists(run / "app0" / "sce_sys" / "param.json")) return false; + const auto metadata = run / "app0" / "sce_sys"; + if (!std::filesystem::exists(metadata / "param.json") && !std::filesystem::exists(metadata / "param.sfo")) return false; std::filesystem::create_directories(run / "data"); return true; } catch (const std::exception&) { diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp new file mode 100644 index 0000000000..bfaf32d9f8 --- /dev/null +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp @@ -0,0 +1,85 @@ +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +constexpr std::uint16_t SfoUtf8 = 0x0204; +constexpr std::uint16_t SfoInteger = 0x0404; +constexpr std::size_t HeaderSize = 20; +constexpr std::size_t EntrySize = 16; + +std::uint16_t readU16(const std::vector& bytes, std::size_t offset) { + std::uint16_t value; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +std::uint32_t readU32(const std::vector& bytes, std::size_t offset) { + std::uint32_t value; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +} + +ParsedParamJson parseParamSfo(const std::filesystem::path& paramSfoPath) { + std::ifstream file(paramSfoPath, std::ios::binary); + if (!file.is_open()) throw std::runtime_error("failed to open param.sfo"); + const std::vector bytes((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (bytes.size() < HeaderSize || std::memcmp(bytes.data(), "\0PSF", 4) != 0) throw std::runtime_error("param.sfo has no PSF header"); + const std::uint64_t keyTable = readU32(bytes, 8); + const std::uint64_t dataTable = readU32(bytes, 12); + const std::uint64_t count = readU32(bytes, 16); + if (count > (bytes.size() - HeaderSize) / EntrySize) throw std::runtime_error("param.sfo tables are out of bounds"); + std::set keys; + std::map strings; + std::map integers; + for (std::uint64_t index = 0; index < count; ++index) { + const auto entry = HeaderSize + index * EntrySize; + const auto keyOffset = keyTable + readU16(bytes, entry); + const auto format = readU16(bytes, entry + 2); + const std::uint64_t length = readU32(bytes, entry + 4); + const auto dataOffset = dataTable + readU32(bytes, entry + 12); + if (keyOffset >= bytes.size() || dataOffset > bytes.size() || length > bytes.size() - dataOffset) throw std::runtime_error("param.sfo entry is out of bounds"); + const auto keyEnd = std::find(bytes.begin() + static_cast(keyOffset), bytes.end(), std::uint8_t{0}); + if (keyEnd == bytes.end()) throw std::runtime_error("param.sfo key is not terminated"); + const std::string key(bytes.begin() + static_cast(keyOffset), keyEnd); + if (!keys.insert(key).second) throw std::runtime_error("param.sfo repeats " + key); + if (format == SfoUtf8) { + const auto begin = bytes.begin() + static_cast(dataOffset); + const auto end = std::find(begin, begin + static_cast(length), std::uint8_t{0}); + if (end == begin + static_cast(length)) throw std::runtime_error("param.sfo " + key + " is not a terminated string"); + strings.emplace(key, std::string(begin, end)); + } else if (format == SfoInteger) { + if (length != 4) throw std::runtime_error("param.sfo " + key + " is not a 32-bit integer"); + integers.emplace(key, static_cast(readU32(bytes, dataOffset))); + } + } + const auto text = [&](const char* key) { + const auto found = strings.find(key); + if (found == strings.end() || found->second.empty()) throw std::runtime_error(std::string("param.sfo is missing ") + key); + return found->second; + }; + ParsedParamJson result; + result.title = text("TITLE"); + result.titleId = text("TITLE_ID"); + if (const auto size = integers.find("DOWNLOAD_DATA_SIZE"); size != integers.end()) { + if (size->second < 0) throw std::runtime_error("param.sfo DOWNLOAD_DATA_SIZE is negative"); + result.downloadDataSizeMiB = static_cast(size->second); + } + for (int index = 0; index < 4; ++index) { + const auto param = integers.find("USER_DEFINED_PARAM_" + std::to_string(index + 1)); + if (param != integers.end()) result.userDefinedParams[index] = param->second; + } + return result; +} diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index b5fee1a726..4f0d237125 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -75,6 +75,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamJsonParser.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamSfoParser.cpp ) target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}) @@ -103,12 +104,12 @@ if(BUILD_TESTING) target_include_directories(download_data_mount_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(download_data_mount_tests PRIVATE libkernel libc) configure_windows_unwind(download_data_mount_tests) - foreach(downloadDataCase IN ITEMS declared:256 none:0 invalid:256 missing:0) + foreach(downloadDataCase IN ITEMS declared:256 none:0 invalid:256 missing:0 sfo:0) string(REPLACE ":" ";" downloadDataCase ${downloadDataCase}) list(GET downloadDataCase 0 downloadDataName) list(GET downloadDataCase 1 downloadDataSize) set(downloadDataRun ${CMAKE_CURRENT_BINARY_DIR}/download_data_mount_${downloadDataName}) - if(downloadDataName STREQUAL "missing") + if(downloadDataName STREQUAL "missing" OR downloadDataName STREQUAL "sfo") file(MAKE_DIRECTORY ${downloadDataRun}) elseif(downloadDataName STREQUAL "invalid") file(WRITE ${downloadDataRun}/app0/sce_sys/param.json "{\"downloadDataSize\":${downloadDataSize}}") @@ -133,6 +134,15 @@ if(BUILD_TESTING) set_tests_properties(param_json_parser PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(param_sfo_parser_tests ${LIBS_INCLUDE_DIR}/tests/ParamSfoParser.cpp) + target_include_directories(param_sfo_parser_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(param_sfo_parser_tests PRIVATE libkernel libc) + configure_windows_unwind(param_sfo_parser_tests) + add_test(NAME param_sfo_parser COMMAND param_sfo_parser_tests) + if(WIN32) + set_tests_properties(param_sfo_parser PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_posix_sem_tests ${LIBS_INCLUDE_DIR}/tests/GuestPosixSem.cpp) target_include_directories(guest_posix_sem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_posix_sem_tests PRIVATE libkernel libc) diff --git a/core/libs/tests/GuestAppInfo.cpp b/core/libs/tests/GuestAppInfo.cpp index e65d891a06..5421462aea 100644 --- a/core/libs/tests/GuestAppInfo.cpp +++ b/core/libs/tests/GuestAppInfo.cpp @@ -26,6 +26,29 @@ struct AppInfo { char unknown2[0x3c]; }; +static void WriteParamSfo(const std::filesystem::path& path) { + const char title[] = "Example"; + const char titleId[] = "CUSA00000"; + const char keys[16] = "TITLE\0TITLE_ID"; + unsigned char bytes[86] = {0, 'P', 'S', 'F', 1, 1, 0, 0, 52, 0, 0, 0, 68, 0, 0, 0, 2, 0, 0, 0, + 0, 0, 4, 2, 8, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, + 6, 0, 4, 2, 10, 0, 0, 0, 10, 0, 0, 0, 8, 0, 0, 0}; + std::memcpy(bytes + 52, keys, sizeof(keys)); + std::memcpy(bytes + 68, title, sizeof(title)); + std::memcpy(bytes + 76, titleId, sizeof(titleId)); + std::ofstream(path, std::ios::binary).write(reinterpret_cast(bytes), sizeof(bytes)); +} + +static int RunSelf(const char* program, std::string_view mode, const std::filesystem::path& directory) { + const std::string arguments = "\"" + std::string(program) + "\" " + std::string(mode) + " \"" + directory.string() + "\""; +#ifdef _WIN32 + const std::string command = "\"" + arguments + "\""; +#else + const std::string command = arguments; +#endif + return std::system(command.c_str()); +} + static std::filesystem::path EnterTemporaryRoot(std::string_view name) { const auto root = std::filesystem::temp_directory_path() / (std::string(name) + std::to_string(getpid_nid_postfix())); std::filesystem::remove_all(root); @@ -40,13 +63,21 @@ int main(int argc, char** argv) { AppInfo info{}; try { sceKernelGetAppInfo(getpid_nid_postfix(), &info); } catch (const std::runtime_error& error) { - return std::string_view(error.what()) == "param.json not found" ? 0 : 1; + return std::string_view(error.what()) == "param.json or param.sfo not found" ? 0 : 1; } return 1; } + if (argc == 3 && std::string_view(argv[1]) == "--with-param-sfo") { + std::filesystem::current_path(argv[2]); + AppInfo info{}; + Require(sceKernelGetAppInfo(getpid_nid_postfix(), &info) == 0); + Require(std::strcmp(info.titleId, "CUSA00000") == 0); + return 0; + } const auto previous = std::filesystem::current_path(); const auto root = EnterTemporaryRoot("guest_app_info_"); std::filesystem::create_directories(root / "app0" / "sce_sys"); + WriteParamSfo(root / "app0" / "sce_sys" / "param.sfo"); std::ofstream(root / "app0" / "sce_sys" / "param.json") << "{\n \"contentId\": \"IV0000-PPSA97358_00-XPSEMU0000000000\",\n \"localizedParameters\": {\"defaultLanguage\": \"en-US\", \"en-US\": {\"titleName\": \"XPSemu\"}},\n \"titleId\": \"PPSA97358\"\n}\n"; AppInfo info; std::memset(&info, 0x5a, sizeof(info)); @@ -63,14 +94,14 @@ int main(int argc, char** argv) { const auto empty = std::filesystem::temp_directory_path() / ("guest_app_info_empty_" + std::to_string(self)); std::filesystem::remove_all(empty); std::filesystem::create_directories(empty); - const std::string arguments = "\"" + std::string(argv[0]) + "\" --without-param-json \"" + empty.string() + "\""; -#ifdef _WIN32 - const std::string command = "\"" + arguments + "\""; -#else - const std::string command = arguments; -#endif - Require(std::system(command.c_str()) == 0); + Require(RunSelf(argv[0], "--without-param-json", empty) == 0); std::filesystem::remove_all(empty); + const auto sfo = std::filesystem::temp_directory_path() / ("guest_app_info_sfo_" + std::to_string(self)); + std::filesystem::remove_all(sfo); + std::filesystem::create_directories(sfo / "app0" / "sce_sys"); + WriteParamSfo(sfo / "app0" / "sce_sys" / "param.sfo"); + Require(RunSelf(argv[0], "--with-param-sfo", sfo) == 0); + std::filesystem::remove_all(sfo); Require(sceKernelUuidCreate(nullptr) == SCE_KERNEL_ERROR_EINVAL); std::uint32_t first[4]{}; diff --git a/core/libs/tests/GuestDownloadDataMount.cpp b/core/libs/tests/GuestDownloadDataMount.cpp index a3ebfccef8..1c3abe8c03 100644 --- a/core/libs/tests/GuestDownloadDataMount.cpp +++ b/core/libs/tests/GuestDownloadDataMount.cpp @@ -1,20 +1,46 @@ #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" +#include #include #include #include +#include #include +#include #include static void Require(bool value) { if (!value) std::abort(); } +static void WriteParamSfo(const std::filesystem::path& path) { + struct Entry { std::uint16_t key, format; std::uint32_t length, maximum, offset; }; + const std::uint32_t header[5] = {0x46535000, 0x0101, 68, 104, 3}; + const Entry entries[3] = {{0, 0x0404, 4, 4, 0}, {19, 0x0204, 8, 8, 4}, {25, 0x0204, 10, 10, 12}}; + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(header), sizeof(header)); + file.write(reinterpret_cast(entries), sizeof(entries)); + file << std::string("DOWNLOAD_DATA_SIZE\0TITLE\0TITLE_ID\0\0\0", 36) << std::string("\0\1\0\0Example\0CUSA00000\0", 22); +} + int main(int argc, char** argv) { Require(argc == 2); const std::string_view mode = argv[1]; - Require(std::filesystem::is_directory("download0") == (mode == "declared")); + Require(std::filesystem::is_directory("download0") == (mode == "declared" || mode == "sfo-child")); const bool data = std::filesystem::is_directory("data"); std::filesystem::remove_all("data"); - Require(data == (mode != "missing")); + Require(data == (mode != "missing" && mode != "sfo")); if (mode == "missing") return 0; + if (mode == "sfo") { + std::filesystem::create_directories("app0/sce_sys"); + WriteParamSfo("app0/sce_sys/param.sfo"); +#ifdef _WIN32 + const std::string command = "\"\"" + std::string(argv[0]) + "\" sfo-child\""; +#else + const std::string command = "\"" + std::string(argv[0]) + "\" sfo-child"; +#endif + const int status = std::system(command.c_str()); + std::filesystem::remove_all("app0"); + Require(status == 0); + return 0; + } if (mode == "invalid") { bool rejected = false; try { @@ -25,6 +51,6 @@ int main(int argc, char** argv) { Require(rejected); return 0; } - Require(std::strcmp(GetAppTitleId_nid_postfix().value, "PPSA00000") == 0); + Require(std::strcmp(GetAppTitleId_nid_postfix().value, mode == "sfo-child" ? "CUSA00000" : "PPSA00000") == 0); std::filesystem::remove_all("download0"); } diff --git a/core/libs/tests/ParamSfoParser.cpp b/core/libs/tests/ParamSfoParser.cpp new file mode 100644 index 0000000000..d4f9ff1c12 --- /dev/null +++ b/core/libs/tests/ParamSfoParser.cpp @@ -0,0 +1,144 @@ +#include "prx/libkernel/AppMetadata/include/ParamSfoParser.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Entry { + std::string key; + std::uint16_t format; + std::vector data; +}; + +void Require(bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +Entry Text(const std::string& key, const std::string& value) { + std::vector data(value.begin(), value.end()); + data.push_back(0); + return {key, 0x0204, data}; +} + +Entry Integer(const std::string& key, std::int32_t value) { + std::vector data(4); + std::memcpy(data.data(), &value, 4); + return {key, 0x0404, data}; +} + +void Put32(std::vector& bytes, std::size_t offset, std::uint32_t value) { + std::memcpy(bytes.data() + offset, &value, 4); +} + +std::vector Build(const std::vector& entries) { + std::vector keys; + std::vector data; + std::vector bytes(20 + entries.size() * 16); + for (std::size_t index = 0; index < entries.size(); ++index) { + const auto entry = 20 + index * 16; + const auto keyOffset = static_cast(keys.size()); + std::memcpy(bytes.data() + entry, &keyOffset, 2); + std::memcpy(bytes.data() + entry + 2, &entries[index].format, 2); + Put32(bytes, entry + 4, static_cast(entries[index].data.size())); + Put32(bytes, entry + 8, static_cast(entries[index].data.size())); + Put32(bytes, entry + 12, static_cast(data.size())); + keys.insert(keys.end(), entries[index].key.begin(), entries[index].key.end()); + keys.push_back(0); + data.insert(data.end(), entries[index].data.begin(), entries[index].data.end()); + } + while (keys.size() % 4 != 0) keys.push_back(0); + std::memcpy(bytes.data(), "\0PSF", 4); + Put32(bytes, 4, 0x0101); + Put32(bytes, 8, static_cast(bytes.size())); + Put32(bytes, 12, static_cast(bytes.size() + keys.size())); + Put32(bytes, 16, static_cast(entries.size())); + bytes.insert(bytes.end(), keys.begin(), keys.end()); + bytes.insert(bytes.end(), data.begin(), data.end()); + return bytes; +} + +ParsedParamJson Parse(const std::filesystem::path& path, const std::vector& bytes) { + { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + Require(static_cast(file), "Cannot write param.sfo fixture"); + } + return parseParamSfo(path); +} + +void Rejects(const std::filesystem::path& path, const std::vector& bytes, const std::string& expected) { + try { + Parse(path, bytes); + } catch (const std::exception& error) { + Require(std::string(error.what()).find(expected) != std::string::npos, "Unexpected error for " + expected + ": " + error.what()); + return; + } + throw std::runtime_error("Accepted invalid param.sfo: " + expected); +} + +std::vector Valid() { + return {Text("CATEGORY", "gd"), Integer("DOWNLOAD_DATA_SIZE", 64), Text("TITLE", "Example"), Text("TITLE_ID", "CUSA00000"), + Integer("USER_DEFINED_PARAM_1", 23566), Integer("USER_DEFINED_PARAM_3", -7), {"PUBTOOLINFO", 0x0004, {1, 2, 3}}}; +} + +} + +int main() { + const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-sfo-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".sfo"); + int result = 0; + try { + const auto parsed = Parse(path, Build(Valid())); + Require(parsed.title == "Example" && parsed.titleId == "CUSA00000", "Incorrect title metadata"); + Require(parsed.downloadDataSizeMiB == 64, "Incorrect download data size"); + Require(parsed.userDefinedParams[0] == 23566 && parsed.userDefinedParams[1] == 0 && parsed.userDefinedParams[2] == -7 && parsed.userDefinedParams[3] == 0, "Incorrect user defined params"); + + auto entries = Valid(); + entries.erase(entries.begin() + 2); + Rejects(path, Build(entries), "missing TITLE"); + entries = Valid(); + entries.erase(entries.begin() + 3); + Rejects(path, Build(entries), "missing TITLE_ID"); + entries = Valid(); + entries[1] = Integer("DOWNLOAD_DATA_SIZE", -1); + Rejects(path, Build(entries), "DOWNLOAD_DATA_SIZE is negative"); + entries = Valid(); + entries[2].data.back() = 'x'; + Rejects(path, Build(entries), "TITLE is not a terminated string"); + entries = Valid(); + entries[4].data.pop_back(); + Rejects(path, Build(entries), "USER_DEFINED_PARAM_1 is not a 32-bit integer"); + entries = Valid(); + entries.push_back(Text("TITLE", "Again")); + Rejects(path, Build(entries), "repeats TITLE"); + + auto bytes = Build(Valid()); + bytes[1] = 'X'; + Rejects(path, bytes, "no PSF header"); + bytes = Build(Valid()); + Put32(bytes, 16, 1000); + Rejects(path, bytes, "tables are out of bounds"); + bytes = Build(Valid()); + Put32(bytes, 20 + 2 * 16 + 12, 0x10000); + Rejects(path, bytes, "entry is out of bounds"); + bytes = Build(Valid()); + Put32(bytes, 20 + 4, 0x10000); + Rejects(path, bytes, "entry is out of bounds"); + bytes = Build(Valid()); + bytes.resize(bytes.size() - 64); + Rejects(path, bytes, "out of bounds"); + Rejects(path, std::vector(8), "no PSF header"); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + result = 1; + } + std::filesystem::remove(path); + if (result == 0) std::cout << "param.sfo parser tests passed\n"; + return result; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c4fe965173..feed86e6c9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -202,3 +202,4 @@ - [rtprio_thread, pthread_setconcurrency, pthread_getconcurrency](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - `rtprio_thread` follows FreeBSD 11.4's `rtprio_thread(2)`. Every guest thread here runs under `SCHED_FIFO` or `SCHED_RR` (priority 256-767), whose rtprio class is real-time; the console's mapping from that priority to the rtprio `prio` range (0-31) is unmeasured, so `RTP_LOOKUP` and `RTP_SET` throw for such a thread rather than report an impossible priority. A thread that is not real-time reports `RTP_PRIO_NORMAL` (3) with 0, and `RTP_SET` accepts only that value. Another function fails with `EINVAL`, a null `rtprio` with `EFAULT`, a thread other than the calling one throws, and an id that is not a live guest thread with `ESRCH`. `pthread_setconcurrency` stores 0 or more and returns `EINVAL` for a negative level (the error itself, as FreeBSD's libthr does, with errno untouched); `pthread_getconcurrency` returns the level, which is a hint and changes nothing about the host's concurrency. Morrowind (`mshivam019/morrowind-ps5`) imports all three at start-up - [sceKernelVirtualQuery](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) returns [VirtualQueryInfo](../../../core/libs/SceTypes.hpp) with a one-byte flag allocation unit so Windows and SysV hosts return the same 72-byte representation, with the name at byte 33 and GPU mask at byte 65. This preserves the existing SysV representation; the console offsets beyond the address, protection and size fields observed in Astro Bot's AGC calls are not hardware-measured. Size and offset assertions plus a raw-byte named-mapping regression guard the host ABI. - [app0.zar files](../../../core/libs/prx/libkernel/File/src/ArchivedFile.cpp) (libkernel) - `sceKernelOpen`, `open`, `sceKernelStat` and `sceKernelCheckReachability` resolve a path missing from `app0/` in `app0.zar`. An archived file is a read-only host null device whose reads, positioned and vectored reads, AIO reads, seeks, `fstat` and `FIONREAD` are served from the archive, with the position shared by `dup`, `dup2` and `F_DUPFD`; writes fail with `EBADF` as on a read-only descriptor, opening one for writing or truncation fails with `EROFS` and `O_CREAT | O_EXCL` with `EEXIST`. `fstat` and `sceKernelStat` report mode `0444` (`0555` for a directory), the node index as inode and the archive file's device and times. Opening an archived directory or a directory present both on disk and in the archive throws, as `getdents` would miss entries. On an archived descriptor `mmap`, `flock`, record locks, `fstatfs`, `ftruncate`, `fsync`, `fdatasync`, `getdirentries`, `fchmod` and `futimes` throw instead of acting on the null device; `mmap` is `noexcept`, so its refusal terminates the process. `lstat` and the other path calls on archived files are not implemented +- [param.sfo](../../../core/libs/prx/libkernel/AppMetadata/src/ParamSfoParser.cpp) (libkernel) is read when `param.json` is absent, for `TITLE`, `TITLE_ID`, `DOWNLOAD_DATA_SIZE` and `USER_DEFINED_PARAM_1` to `_4`, and mounts `/data` and `/download0` like `param.json`; the localized `TITLE_00` to `TITLE_30` are not selected by the console language. From ded05f528736726c8b6baca6e3d9d6d921b0408c Mon Sep 17 00:00:00 2001 From: BasilF Date: Sat, 17 Oct 2026 05:27:32 +0300 Subject: [PATCH 070/131] fix(agc): defer unrouted vertex barriers --- .../src/Driver/Shaders/ShaderRegistry.cpp | 8 ++++- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 31 +++++++++++++++++++ 2 files changed, 38 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 ab8df700c0..d724113bf0 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -834,7 +834,13 @@ std::unique_ptr PlanRegistered(const ShaderSnapshot& snap } else { const auto routing = RegisterValue(state.context, 0x2d5); wave = (routing & 0x00400000u) != 0 ? 32u : 64u; - if (registration && stage == Stage::Vertex && (routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) return nullptr; + const bool unroutedBarrier = routing == 0 && std::ranges::any_of(decoded.instructions, [](const auto& instruction) { + return instruction.op == ShaderRecompiler::RdnaOpcode::SBarrier; + }); + if (registration && stage == Stage::Vertex && (((routing & 0x02002024u) == 0x2000u && ProgramUsesWorkgroup(snapshot, codeOffset)) || unroutedBarrier)) { + if (unroutedBarrier && deferred != nullptr) return defer("its s_barrier depends on draw routing"); + return nullptr; + } if ((routing & 0x20u) != 0 || stage == Stage::Mesh || (routing & 4u) != 0 || ((routing >> 3u) & 3u) == 1u) { if (registration) return {}; auto stageState = state; diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index f4fe928120..6245634631 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -784,6 +784,36 @@ void testRegisteredVertexWorkgroupRouting() { check(expectFailure([&] { static_cast(VertexWorkgroupRequired(queue, registry)); }).find("reserved graphics program address") != std::string::npos, "reserved NGG program address bits were accepted"); } +void testRegisteredFastLaunchWorkgroupVertex() { + alignas(256) static const std::array code{ + 0xd8340000u, 0x00000100u, 0xbf8a0000u, 0xf80008cfu, 0x00000000u, 0xbf810000u, + }; + struct Header { + Shader shader{}; + ShaderUserData userData{}; + std::array registers{}; + }; + alignas(8) static Header header{}; + const auto address = reinterpret_cast(code.data()); + header.shader.file_header = 0x34333231u; + header.shader.version = 0x18u; + header.shader.header_size = sizeof(Header); + header.shader.shader_size = sizeof(code); + header.shader.code = code.data(); + header.shader.type = 2u; + header.shader.user_data = &header.userData; + header.registers = {{{0x0c8u, static_cast(address >> 8u)}, {0x0c9u, static_cast(address >> 40u)}, {0x08au, 0x022c0245u}, {0x08bu, 0x0009002au}}}; + header.shader.sh_registers = header.registers.data(); + header.shader.num_sh_registers = header.registers.size(); + AgcDriverRegisterShader_nid_postfix(&header.shader); + const std::array stages{&header.shader}; + const std::array context{{ + {0x2d5u, 0x00482030u}, {0x1ffu, 32u}, {0x2ceu, 32u}, {0x2d3u, 64u}, {0x29bu, 2u}, {0x2abu, 4u}, + }}; + const std::array primitive{{{0x242u, 1u}, {0x25bu, 0x201u}}}; + AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, context, primitive); +} + void testWorkerFailure() { std::array words{0xc0031500, 1, 1, 1, 0x41}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; @@ -886,6 +916,7 @@ int main(int argc, char** argv) { testProgramWorkgroupUse(); testVertexTailSnapshots(); testRegisteredVertexWorkgroupRouting(); + testRegisteredFastLaunchWorkgroupVertex(); testEvents(); testValidation(); testClearState(); From d76ea9a6bede961f431dd8072f5ae518b991491e Mon Sep 17 00:00:00 2001 From: Nithishvelan M Date: Sat, 17 Oct 2026 08:38:06 +0530 Subject: [PATCH 071/131] docs: drop unverified note on macOS host game execution --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57c93abbf4..5777022fb4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Tool for automatic executables porting to Linux and Windows. -The relinker-only build runs on macOS, including Apple silicon. Game execution on a macOS host is not yet supported. +The relinker-only build runs on macOS, including Apple silicon. [**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device. From 2aa6bfca62e750b3d9118aa38d60bdb27fa0a513 Mon Sep 17 00:00:00 2001 From: Lberonio <103309975+Lberonio@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:09:14 +0300 Subject: [PATCH 072/131] fix(agc): suppress the flip end-of-pipe event on queues that raise release interrupts --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +- .../prx/libSceAgcDriver/Eq/include/Event.hpp | 2 +- .../libs/prx/libSceAgcDriver/Eq/src/Event.cpp | 5 ++- .../Execution/include/Driver/Driver.hpp | 5 +++ .../src/Driver/Packets/PacketExecution.cpp | 22 ++++++++-- .../src/Driver/Packets/PacketMemory.cpp | 6 +-- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 40 ++++++++++++------- docs/dev/debt/libSceAgcDriver.md | 2 +- 8 files changed, 60 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 4eba5fa87e..56bae526f1 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1541,8 +1541,10 @@ if(BUILD_TESTING) endforeach() add_test(NAME agc_driver_readonly_resummarize COMMAND agc_driver_tests --readonly-resummarize) set_tests_properties(agc_driver_readonly_resummarize PROPERTIES TIMEOUT 10) + add_test(NAME agc_driver_eop_interrupts COMMAND agc_driver_tests --eop-interrupts) + set_tests_properties(agc_driver_eop_interrupts PROPERTIES TIMEOUT 60) if(WIN32) - set_tests_properties(agc_driver_readonly_resummarize PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_readonly_resummarize agc_driver_eop_interrupts PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests) diff --git a/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp b/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp index 3cc1ebdff5..2b7bcbe4b0 100644 --- a/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp +++ b/core/libs/prx/libSceAgcDriver/Eq/include/Event.hpp @@ -4,7 +4,7 @@ #include #include "SceTypes.hpp" -void AgcDriverDeliverEopInterrupt(std::uint32_t queue); +bool AgcDriverDeliverEopInterrupt(std::uint32_t queue); extern "C" int APS5_VABI sceAgcDriverAddEqEvent(KernelEqueue eq, int id, void* udata); extern "C" int APS5_VABI sceAgcDriverDeleteEqEvent(KernelEqueue eq, int id); diff --git a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp index 564ec3806d..80774d7216 100644 --- a/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp +++ b/core/libs/prx/libSceAgcDriver/Eq/src/Event.cpp @@ -25,17 +25,20 @@ std::vector g_registrations; } -void AgcDriverDeliverEopInterrupt(std::uint32_t queue) { +bool AgcDriverDeliverEopInterrupt(std::uint32_t queue) { std::vector registrations; { std::lock_guard lock(g_mutex); registrations = g_registrations; } + bool delivered = false; for (const auto& registration : registrations) { if (registration.id == static_cast(queue)) { EqueueTriggerEvent_nid_postfix(registration.eq, static_cast(registration.id), EvfiltGraphicsCore, nullptr); + delivered = true; } } + return delivered; } extern "C" { diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 4bceb20d24..9c2fbd8573 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -314,6 +314,8 @@ private: FrameTiming* frameTiming(); FrameTiming* includeTimingSubmission(const Submission& submission, bool firstSegment); void execute(const Submission& submission); + void deliverGuestEopInterrupt(std::uint32_t queue); + void deliverFlipEopInterrupt(std::uint32_t queue); void markCompleted(std::uint64_t serial); static const std::atomic*& workerQueued(); void reapCompletionLabels(); @@ -331,6 +333,9 @@ private: std::atomic flipSerial{0}; std::atomic flipBatchesUnsignaled{0}; + std::mutex eopSelfSignalMutex; + std::unordered_set eopSelfSignalQueues; + alignas(64) std::atomic packetsInFlight{0}; std::atomic packetsDone{0}; 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 02458b6484..a259b78eef 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -42,6 +42,20 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head } +void Driver::deliverGuestEopInterrupt(std::uint32_t queue) { + if (!AgcDriverDeliverEopInterrupt(queue)) return; + std::lock_guard lock(eopSelfSignalMutex); + eopSelfSignalQueues.insert(queue); +} + +void Driver::deliverFlipEopInterrupt(std::uint32_t queue) { + { + std::lock_guard lock(eopSelfSignalMutex); + if (eopSelfSignalQueues.count(queue) != 0) return; + } + AgcDriverDeliverEopInterrupt(queue); +} + template void Driver::timed(double WorkerProfile::*bucket, TWork&& work) { static thread_local WorkerProfile profile; @@ -273,7 +287,7 @@ void Driver::execute(const Submission& submission) { Pm4::Execute(packet, queue); if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue); } - if (endOfPipeInterrupt && !interruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue); + if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue); } if (deviceUse.owns_lock()) deviceUse.unlock(); CheckFailure(); @@ -289,10 +303,10 @@ void Driver::execute(const Submission& submission) { if (localDevice != nullptr) { localDevice->FlipBatches(batchesAtFlip, unsignaledAtFlip); const auto queueId = submission.queue; - flipInterruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, false); + flipInterruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverFlipEopInterrupt(queueId); }, false); } } - if (!flipInterruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue); + if (!flipInterruptDeferred) deliverFlipEopInterrupt(submission.queue); ++flipsCounted; if (batchesAtFlip != 0) flipSerial = batchesAtFlip; flipBatchesUnsignaled += unsignaledAtFlip; @@ -393,7 +407,7 @@ void Driver::execute(const Submission& submission) { if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue); } } - if (endOfPipeInterrupt && !interruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue); + if (endOfPipeInterrupt && !interruptDeferred) deliverGuestEopInterrupt(submission.queue); } if (drawPacket || (sampleDump && wroteOnGpu)) Graphics::Recorder::CountRecordedWork(); timing.Mark(header == RenderingWaitPacketHeader ? "rendering_wait" : opcode == 0x3c || opcode == 0x93 ? "memory_wait" : opcode == 0x15 || opcode == 0x16 ? "dispatch" : drawPacket ? "draw" : "execute"); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index 452c674ab3..c1a63f95b4 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -54,7 +54,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue } if (reason == 0 || reason == 5) { const auto queueId = submission.queue; - interruptDeferred = localDevice->AfterRecordedWork([queueId] { AgcDriverDeliverEopInterrupt(queueId); }, submission.queue == 0); + interruptDeferred = localDevice->AfterRecordedWork([this, queueId] { deliverGuestEopInterrupt(queueId); }, submission.queue == 0); if (interruptDeferred && workOpen) localDevice->SubmitRecorded(submission.queue == 0); wroteOnGpu = true; } else if (reason == 1) { @@ -397,10 +397,10 @@ void Driver::commitLabel(std::uint32_t queue, std::uint64_t address, std::spanAfterRecordedWork([queue] { AgcDriverDeliverEopInterrupt(queue); }, queue == 0); + deferred = localDevice->AfterRecordedWork([this, queue] { deliverGuestEopInterrupt(queue); }, queue == 0); if (deferred && workOpen) localDevice->SubmitRecorded(queue == 0); } - if (!deferred) AgcDriverDeliverEopInterrupt(queue); + if (!deferred) deliverGuestEopInterrupt(queue); } submitDueAfterCommit(localDevice.get()); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index f4fe928120..d47d51e8ce 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -195,11 +195,26 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverAddEqEvent(eq, 0, &graphicsTag) == 0, "graphics event registration failed"); check(sceAgcDriverAddEqEvent(eq, 0x20, &computeTag) == 0, "compute event registration failed"); expectFailure([] { sceAgcDriverAddEqEvent(0, 0, nullptr); }); + struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this { + std::atomic ready{0}; + std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); } + void GpuReady(const std::shared_ptr&) override { ready.fetch_add(1); } + void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); } + }; + const auto output = std::make_shared(); + AgcDriverRegisterVideoOutput_nid_postfix(7, output); + std::array flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0}; + Packet flipPacket{flip.data(), static_cast(flip.size()), 0, {}}; + std::array events{}; + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip-only queue submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->ready.load() == 1, "flip-only queue flip was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip on a queue that never requested a release interrupt did not raise its event"); + check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip-only queue event was not cleared"); std::array words{0xc0064900, 0, 1u << 24u, 0, 0, 0, 0, 0}; Packet packet{words.data(), static_cast(words.size()), 0, {}}; check(sceAgcDriverSubmitDcb(&packet) == 0 && sceAgcDriverSubmitDcb(&packet) == 0, "interrupt submit failed"); AgcDriverWaitIdle_nid_postfix(); - std::array events{}; check(owner->GetTriggeredEvents(events.data(), 2) == 1, "graphics end-of-pipe interrupt was not delivered to its queue only"); check(events[0].filter == -14 && events[0].udata == &graphicsTag && events[0].data == 2 && sceAgcDriverGetEqEventType(events.data()) == 0, "graphics end-of-pipe event encoding is wrong"); check(sceAgcDriverGetEqContextId(events.data()) == 0, "graphics end-of-pipe event reports another context"); @@ -212,21 +227,10 @@ void testEndOfPipeInterrupts() { check(sceAgcDriverSubmitDcb(&packet) == 0, "plain release submit failed"); AgcDriverWaitIdle_nid_postfix(); check(owner->GetTriggeredEvents(events.data(), 2) == 0, "release without INT_SEL raised an interrupt"); - struct Output final : AgcDriver::IVideoOutput, AgcDriver::IFlipRequest, std::enable_shared_from_this { - std::atomic ready{0}; - std::shared_ptr Reserve(const AgcDriver::FlipInfo&) override { return shared_from_this(); } - void GpuReady(const std::shared_ptr&) override { ready.fetch_add(1); } - void Fail(std::exception_ptr error) noexcept override { if (!error) std::terminate(); } - }; - const auto output = std::make_shared(); - AgcDriverRegisterVideoOutput_nid_postfix(7, output); - std::array flip{AgcDriver::FlipPacketHeader, 7, 0xfffffffeu, 1, 0, 0}; - Packet flipPacket{flip.data(), static_cast(flip.size()), 0, {}}; - check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip submit failed"); + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after a release interrupt submit failed"); AgcDriverWaitIdle_nid_postfix(); - check(output->ready.load() == 1, "flip was not handed to its output"); - check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip did not raise the graphics end-of-pipe event"); - check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip event was not cleared"); + check(output->ready.load() == 2, "flip after a release interrupt was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip on a queue that already raised its own release interrupt raised a second event"); AgcDriverUnregisterVideoOutput_nid_postfix(7, output); alignas(8) static volatile std::uint64_t label = 0; const auto labelAddress = reinterpret_cast(&label); @@ -804,6 +808,12 @@ void testWorkerFailure() { int main(int argc, char** argv) { try { + if (argc == 2 && std::string_view(argv[1]) == "--eop-interrupts") { + testEndOfPipeInterrupts(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC end-of-pipe interrupt queue mode tests passed"); + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--readonly-resummarize") { testReadOnlyResummarize(); LibcRunShutdown_nid_postfix(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 71210637ea..67062ac81a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -163,6 +163,6 @@ - [Descriptor pool recycling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. -- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. +- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing after that. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Gran Turismo 7 (PPSA01317) and PPSA24156 pair more flips than release interrupts per frame, so the extra flip event reaches a consumer with no matching context and crashes. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. From 3573289f2f5add4a236cf11e40e718079446485c Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 23:16:00 +0000 Subject: [PATCH 073/131] fix(libSceNet): set an option on a libkernel socket through sceNetSetsockopt MXGP 26 (PPSA30839) opens TCP sockets with libkernel's socket() and turns on TCP_NODELAY with sceNetSetsockopt. libSceNet only looked in its own socket table, so the call failed with SCE_NET_ERROR_EBADF. On the console both calls work on the same kernel sockets. A descriptor libSceNet does not own but libkernel has open as a socket now goes to libkernel's setsockopt(), which answers the options it models. Anything else still fails with EBADF. The fallback runs after g_mutex is released. guest_sce_net sets SO_SNDBUF on a libkernel TCP socket through sceNetSetsockopt, and checks that the call fails with EBADF once the socket is closed. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 14 ++++++++++++-- core/libs/tests/GuestSceNet.cpp | 10 ++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 8fd8578762..1bb330b707 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -36,6 +36,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -1146,13 +1147,22 @@ int APS5_VABI sceNetShutdown(int s, int how) { return error == 0 ? 0 : fail(error); } +extern "C" int APS5_VABI setsockopt_nid_postfix(int descriptor, int level, int option, const void* value, std::uint32_t length); + int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval, uint32_t optlen) { Sock socket; + bool found = false; { std::lock_guard lk(g_mutex); auto it = g_socks.find(s); - if (it == g_socks.end()) return fail(NET_EBADF); - socket = it->second; + if (it != g_socks.end()) { + socket = it->second; + found = true; + } + } + if (!found) { + if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); + return setsockopt_nid_postfix(s, level, optname, optval, optlen) == 0 ? 0 : fail(*__error_nid_postfix()); } if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); if (optname == NET_SO_RCVTIMEO || optname == NET_SO_SNDTIMEO) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e5b47cdeae..c7fec08d99 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -855,6 +855,15 @@ static void CheckStreamAddressLengths() { for (const int descriptor : {accepted, client, listener}) Require(sceNetSocketClose(descriptor) == 0); } +static void CheckKernelSocketOption() { + const int kernel = socket_nid_postfix(2, 1, 0); + Require(kernel >= 0); + const int size = 65536; + Require(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)) == 0); + Require(close_nid_postfix(kernel) == 0); + Require(Failed(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)), 9)); +} + static void CheckBufferSizes() { const int socket = sceNetSocket("buffer-sizes", 2, 2, 17); Require(socket >= 0); @@ -1106,6 +1115,7 @@ int main() { CheckStreamAddressLengths(); CheckBufferSizes(); CheckOnesBroadcast(); + CheckKernelSocketOption(); const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); From 9c9311af8f89c95693da4e9f7b0e680b7cf7c231 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Fri, 16 Oct 2026 14:11:02 +0000 Subject: [PATCH 074/131] test(libSceNet): set TCP_NODELAY on a libkernel socket through sceNetSetsockopt The call MXGP 26 makes; libkernel answers it since #5565. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestSceNet.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index c7fec08d99..68f3dabef2 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -860,6 +860,8 @@ static void CheckKernelSocketOption() { Require(kernel >= 0); const int size = 65536; Require(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)) == 0); + const int one = 1; + Require(sceNetSetsockopt(kernel, 6, 1, &one, sizeof(one)) == 0); Require(close_nid_postfix(kernel) == 0); Require(Failed(sceNetSetsockopt(kernel, 0xffff, 0x1001, &size, sizeof(size)), 9)); } From 8f37cddff0e38bbb46cf96c79d13fb9459f03a57 Mon Sep 17 00:00:00 2001 From: fma-rc <188743521+fma-rc@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:52:47 +0200 Subject: [PATCH 075/131] feat(agc): link calls through the AGC FS table --- .../Execution/src/Driver/Lifecycle.cpp | 45 +++++++ .../src/Driver/Shaders/ShaderRegistry.cpp | 2 +- .../tests/execution/ComputeFsTableCalls.cpp | 114 ++++++++++++++++++ core/libs/tests/ScalarCall.cpp | 18 +++ .../include/ControlFlow/GraphBuilder.hpp | 1 + .../ControlFlow/src/GraphBuilder.cpp | 34 ++++-- docs/dev/debt/libSceAgcDriver.md | 3 +- 7 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp index 91c50d6f3f..1597ddf697 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp @@ -13,10 +13,13 @@ #include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "ShaderDiskCache.hpp" +#include "prx/libc/include/GuestArena.hpp" #include #include #include +#include #include +#include #include #include #ifdef _WIN32 @@ -26,6 +29,7 @@ #include #else #include +#include #endif namespace AgcDriver::DriverDetail { @@ -48,6 +52,45 @@ void PrintFailure(std::exception_ptr error) { std::fflush(stderr); } +constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull; +constexpr std::size_t DriverMemoryBytes = 2u << 20u; +constexpr std::uint64_t FsTableAddress = 0xfe0040000ull; +constexpr std::uint64_t FsTableSlotBytes = 16u; +constexpr std::uint64_t FsTableSlots = 6u; +constexpr std::uint64_t FsRoutineRecords = 8u; +constexpr std::uint64_t FsRoutineRecordBytes = 16u; +constexpr std::uint64_t FsRoutineTableBytes = FsRoutineRecords * FsRoutineRecordBytes; +constexpr std::uint64_t FsRoutineTableAddress = FsTableAddress + FsTableSlots * FsTableSlotBytes; +constexpr std::uint64_t FsRoutineAddress = FsRoutineTableAddress + FsRoutineTableBytes; +constexpr std::uint32_t ScalarSetpcB64LinkS14 = 0xbe80200eu; +constexpr std::uint32_t ScalarEndProgram = 0xbf810000u; +constexpr std::uint64_t FsTableDword3 = 0x5204u; + +void MapDriverMemory() { + void* mapped; +#ifdef _WIN32 + mapped = reinterpret_cast(DriverMemoryAddress); + GuestArena::GuestArenaCommit_nid_postfix(mapped, DriverMemoryBytes, PAGE_READWRITE, DriverMemoryBytes); +#else + mapped = mmap(reinterpret_cast(DriverMemoryAddress), DriverMemoryBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + if (mapped == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map the AGC driver memory"); +#endif + if (reinterpret_cast(mapped) != DriverMemoryAddress) throw std::runtime_error("the AGC driver memory was not mapped at its fixed address"); + auto* routineTable = reinterpret_cast(FsRoutineTableAddress); + for (std::uint64_t record = 0; record < FsRoutineRecords; ++record) { + routineTable[record * 2u] = FsRoutineAddress; + routineTable[record * 2u + 1u] = 0u; + } + auto* routine = reinterpret_cast(FsRoutineAddress); + routine[0] = ScalarSetpcB64LinkS14; + routine[1] = ScalarEndProgram; + auto* slot = reinterpret_cast(FsTableAddress); + slot[0] = (FsRoutineTableAddress & 0xffffffffffffull) | 16ull << 48u; + slot[1] = FsTableDword3 << 32u | FsRoutineRecords; + GuestAllocations::Mutation mutation; + mutation.Add(mapped, DriverMemoryBytes, true, true, true); +} + bool StackHoldsImport() { #ifdef _WIN32 ULONG_PTR low = 0; @@ -138,6 +181,8 @@ void Driver::stop() { Driver::Driver() { InstallAsyncLog_nid_no_patch(); + static const bool driverMemory = (MapDriverMemory(), true); + static_cast(driverMemory); static_cast(GuestMemory::TrackerGeneration()); ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs); try { 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 1b1299da12..b8574aac1f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -586,7 +586,7 @@ std::vector ReadTableCallees(const ShaderRecompil constexpr std::uint64_t limitBytes = 64u << 20u; constexpr std::size_t limitCallees = 64u; const auto slot = call.userDataRegister - request.context.userDataBaseRegister; - const auto pointer = static_cast(request.context.userData[slot]) | static_cast(request.context.userData[slot + 1u]) << 32u; + const auto pointer = call.constantPointer ? *call.constantPointer : static_cast(request.context.userData[slot]) | static_cast(request.context.userData[slot + 1u]) << 32u; const auto words = ReadGuestBytes(pointer + call.descriptorOffset, 16u, "call table descriptor"); ShaderRecompiler::ShaderBufferResource descriptor{}; std::memcpy(descriptor.fields.data(), words.data(), words.size()); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp new file mode 100644 index 0000000000..923b3b7212 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ComputeFsTableCalls.cpp @@ -0,0 +1,114 @@ +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "SceShaders.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; + +constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Untouched = 0xcafebabeu; +constexpr std::uint32_t Marker = 0x55u; +constexpr std::uint64_t FsTable = 0xfe0040000ull; + +alignas(256) constexpr std::array Caller{ + 0x34040082u, 0x7e0202ffu, 0x00000055u, 0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080235u, 0xfa000000u, 0xbf8cc07fu, 0xf4240384u, + 0xfa000020u, 0xbf8cc07fu, 0xbf130e80u, 0xbf840006u, 0xf4240404u, 0xfa000028u, 0xbf8cc07fu, 0xbe8e210eu, 0xe0701000u, 0x80010102u, + 0xbf810000u, 0xbf9f0000u}; +alignas(256) std::array Output{}; + +std::vector Commands() { + 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{Threads, 1, 1}; + registers(0x207, shape); + const auto address = reinterpret_cast(Caller.data()); + const std::array program{static_cast(address >> 8u), static_cast(address >> 40u)}; + registers(0x20c, program); + const std::array resource{8u << 1u}; + registers(0x213, resource); + const auto output = reinterpret_cast(Output.data()); + const std::array data{0u, 0u, 0u, 0u, static_cast(output), static_cast((output >> 32u) & 0xffffu), + static_cast(sizeof(Output)), 0x31016facu}; + registers(0x240, data); + words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u}); + return words; +} + +std::string Dispatch() { + Output.fill(Untouched); + const auto words = Commands(); + Packet packet{const_cast(words.data()), static_cast(words.size()), 0, {}}; + std::string failure; + try { + AgcDriver::Submit(&packet, 0); + AgcDriverWaitIdle_nid_postfix(); + } catch (const std::exception& error) { + failure = error.what(); + } + AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast(Output.data()), sizeof(Output)); + return failure; +} + +void CheckTable() { + AgcDriverWaitIdle_nid_postfix(); + Require(AgcDriver::GuestMemory::Accessible(reinterpret_cast(FsTable), 0x60u), "the FS table at 0xfe0040000 is not mapped"); + std::array slots{}; + std::memcpy(slots.data(), reinterpret_cast(FsTable), sizeof(slots)); + const auto records = slots[0] & 0xffffffffffffull; + Require(slots[0] >> 48u == 16u, "FS table slot 0 does not have a stride of 16"); + Require(slots[1] == 0x0000520400000008ull, "FS table slot 0 does not describe eight records with dword3 0x5204"); + for (std::size_t slot = 2; slot < slots.size(); ++slot) Require(slots[slot] == 0u, "an FS table slot other than the first is not zero"); + Require(records % 16u == 0u && AgcDriver::GuestMemory::Accessible(reinterpret_cast(records), 128u), "the FS table records are not readable"); + std::array table{}; + std::memcpy(table.data(), reinterpret_cast(records), sizeof(table)); + for (std::size_t record = 0; record < 8u; ++record) { + Require(table[record * 2u] != 0u && table[record * 2u] == table[2u] && table[record * 2u + 1u] == 0u, "an FS table record does not hold the routine and a zero"); + } + std::array routine{}; + 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]"); +} + +} + +int main() { + try { + { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + } + CheckTable(); + const auto failure = Dispatch(); + Require(failure.find("not statically resolvable") == std::string::npos, "the call through the FS table did not link: " + failure); + Require(failure.empty(), "the dispatch failed: " + failure); + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == Marker, "lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + " after the call through the FS table, expected " + std::to_string(Marker)); + } + AgcDriverShutdown_nid_postfix(); + std::puts("compute FS table call tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + try { + AgcDriverShutdown_nid_postfix(); + } catch (const std::exception&) { + } + return 1; + } +} diff --git a/core/libs/tests/ScalarCall.cpp b/core/libs/tests/ScalarCall.cpp index dda7b2fea4..4dae722b17 100644 --- a/core/libs/tests/ScalarCall.cpp +++ b/core/libs/tests/ScalarCall.cpp @@ -198,7 +198,25 @@ static IrProgram TranslateCode(std::span code, const Swappc return InstructionTranslator{}.Translate(program, cfg, options); } +static void CheckConstantTableCall() { + const std::vector caller{0xbeea03ffu, 0xe0040000u, 0xbeeb038fu, 0xf4080135u, 0xfa000000u, 0xbf8cc07fu, 0xf4240382u, 0xfa000020u, 0xbf8cc07fu, 0xbf130e80u, + 0xbf840004u, 0xf4240402u, 0xfa000028u, 0xbf8cc07fu, 0xbe8e210eu, 0xbf810000u}; + const SwappcInfo swappc{false, 0u, 16u}; + const auto calls = FindTableCalls(RdnaInstructionDecoder{}.Decode(caller), swappc); + Require(calls.size() == 1u); + Require(calls[0].constantPointer == std::optional(0xfe0040000ull) && calls[0].descriptorOffset == 0u && calls[0].entryOffset == 0x20u && calls[0].entryStride == 0u); + Require(calls[0].linkRegister == 14u && calls[0].targetRegister == 14u); + auto changedHigh = caller; + changedHigh[2] = 0xbeeb0380u; + const auto zero = FindTableCalls(RdnaInstructionDecoder{}.Decode(changedHigh), swappc); + Require(zero.size() == 1u && zero[0].constantPointer == std::optional(0xe0040000ull)); + auto computed = caller; + computed[2] = 0xbeeb0302u; + Require(FindTableCalls(RdnaInstructionDecoder{}.Decode(computed), swappc).empty()); +} + static void CheckTableCalls() { + CheckConstantTableCall(); const SwappcInfo swappc{false, 0u, 16u}; const auto caller = TableCaller(); const auto program = RdnaInstructionDecoder{}.Decode(caller); diff --git a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp index 49d52a67f0..1f333297f7 100644 --- a/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp +++ b/core/shader/recompiler/ControlFlow/include/ControlFlow/GraphBuilder.hpp @@ -51,6 +51,7 @@ struct TableCall { std::uint32_t descriptorOffset = 0; std::uint32_t entryOffset = 0; std::uint32_t entryStride = 0; + std::optional constantPointer; }; struct TableCallee { diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index a67f30d1ec..fe2c3565a3 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -294,10 +294,11 @@ bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index, std:: return covers(instruction.destination) || covers(instruction.destination2); } -bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u) { +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer, std::uint32_t waveSize = 0u, std::uint32_t skipBranchesPast = 0u) { for (auto position = end; position-- > 0u;) { const auto& instruction = program.instructions[position]; - if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false; + const bool skipped = skipBranchesPast != 0u && IsDirectBranchOpcode(instruction.op) && instruction.op != RdnaOpcode::SBranch && instruction.branchTarget > skipBranchesPast; + if ((IsDirectBranchOpcode(instruction.op) && !skipped) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false; if (std::ranges::any_of(indices, [&](std::uint32_t index) { return writesScalar(instruction, index, waveSize); })) { writer = position; return true; @@ -1286,10 +1287,10 @@ bool isScalarBufferLoad(RdnaOpcode op) { } } -bool loadedDword(const RdnaProgram& program, std::uint32_t reg, std::uint32_t before, std::uint32_t waveSize, std::uint32_t& load, std::uint32_t& dword) { +bool loadedDword(const RdnaProgram& program, std::uint32_t reg, std::uint32_t before, std::uint32_t waveSize, std::uint32_t skipBranchesPast, std::uint32_t& load, std::uint32_t& dword) { for (std::uint32_t copies = 0; copies < 4u; ++copies) { std::uint32_t writer = 0; - if (!findLastWriter(program, before, {reg}, writer, waveSize)) return false; + if (!findLastWriter(program, before, {reg}, writer, waveSize, skipBranchesPast)) return false; const auto& instruction = program.instructions[writer]; const auto destination = scalarIndex(instruction.destination); const auto source = scalarIndex(instruction.source0); @@ -1445,28 +1446,39 @@ std::vector FindTableCalls(const RdnaProgram& program, const SwappcIn const auto target = scalarIndex(instruction.source0); if (link == NoScalarRegister || link % 2u != 0u || link > 104u || target == NoScalarRegister || target % 2u != 0u || target > 104u) continue; std::uint32_t lowLoad = 0, lowDword = 0, highLoad = 0, highDword = 0; - if (!loadedDword(program, target, index, swappc.waveSize, lowLoad, lowDword) || !loadedDword(program, target + 1u, index, swappc.waveSize, highLoad, highDword)) continue; + const auto reader = instruction.programCounter; + if (!loadedDword(program, target, index, swappc.waveSize, reader, lowLoad, lowDword) || !loadedDword(program, target + 1u, index, swappc.waveSize, reader, highLoad, highDword)) continue; if (lowLoad != highLoad || highDword != lowDword + 1u || !sameBlock(lowLoad, index)) continue; const auto& load = program.instructions[lowLoad]; const auto descriptor = scalarIndex(load.source0); if (load.source0.kind != RdnaOperandKind::ScalarRegister || descriptor % 4u != 0u || load.memoryOffset >= 0x100000u || (load.memoryOffset & 3u) != 0u) continue; std::uint32_t descriptorLoad = 0; - if (!findLastWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize) || !sameBlock(descriptorLoad, index)) continue; + if (!findLastWriter(program, lowLoad, {descriptor, descriptor + 1u, descriptor + 2u, descriptor + 3u}, descriptorLoad, swappc.waveSize, reader) || !sameBlock(descriptorLoad, index)) continue; const auto& table = program.instructions[descriptorLoad]; const auto pointer = scalarIndex(table.source0); - if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || table.source0.kind != RdnaOperandKind::ScalarRegister || table.source1.kind != RdnaOperandKind::Null || - table.memoryOffset >= 0x100000u || (table.memoryOffset & 3u) != 0u || !userData(pointer) || !userData(pointer + 1u) || - !unchangedUntil(program, pointer, descriptorLoad, swappc.waveSize) || !unchangedUntil(program, pointer + 1u, descriptorLoad, swappc.waveSize)) continue; + if (table.op != RdnaOpcode::SLoadDwordx4 || !isScalar(table.destination, descriptor) || pointer == NoScalarRegister || pointer % 2u != 0u || table.source1.kind != RdnaOperandKind::Null || + table.memoryOffset >= 0x100000u || (table.memoryOffset & 3u) != 0u) continue; + std::optional constantPointer; + if (!(userData(pointer) && userData(pointer + 1u) && unchangedUntil(program, pointer, descriptorLoad, swappc.waveSize) && unchangedUntil(program, pointer + 1u, descriptorLoad, swappc.waveSize))) { + std::uint32_t lowWriter = 0, highWriter = 0, low = 0, high = 0; + if (!findLastWriter(program, descriptorLoad, {pointer}, lowWriter, swappc.waveSize, reader) || !findLastWriter(program, descriptorLoad, {pointer + 1u}, highWriter, swappc.waveSize, reader) || + !sameBlock(std::min(lowWriter, highWriter), descriptorLoad)) continue; + const auto& lowMove = program.instructions[lowWriter]; + const auto& highMove = program.instructions[highWriter]; + if (lowMove.op != RdnaOpcode::SMovB32 || highMove.op != RdnaOpcode::SMovB32 || !isScalar(lowMove.destination, pointer) || !isScalar(highMove.destination, pointer + 1u) || + !isImmediate(lowMove.source0, low) || !isImmediate(highMove.source0, high)) continue; + constantPointer = static_cast(high) << 32u | low; + } std::uint32_t stride = 0; if (load.source1.kind != RdnaOperandKind::Null) { const auto offset = scalarIndex(load.source1); std::uint32_t shift = 0, shiftIndex = 0; - if (offset == NoScalarRegister || !findLastWriter(program, lowLoad, {offset}, shiftIndex, swappc.waveSize) || !sameBlock(shiftIndex, index)) continue; + if (offset == NoScalarRegister || !findLastWriter(program, lowLoad, {offset}, shiftIndex, swappc.waveSize, reader) || !sameBlock(shiftIndex, index)) continue; const auto& shifted = program.instructions[shiftIndex]; if (shifted.op != RdnaOpcode::SLshlB32 || !isScalar(shifted.destination, offset) || !isImmediate(shifted.source1, shift) || shift < 2u || shift > 24u) continue; stride = 1u << shift; } - calls.push_back({instruction.programCounter, link, target, pointer, table.memoryOffset, load.memoryOffset + lowDword * 4u, stride}); + calls.push_back({instruction.programCounter, link, target, constantPointer ? 0u : pointer, table.memoryOffset, load.memoryOffset + lowDword * 4u, stride, constantPointer}); } return calls; } diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 71210637ea..6211a42180 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -156,7 +156,8 @@ - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) whose record range overlaps a buffer or storage image written by its own dispatch faults (`table written by its dispatch`) when any entry is selected, even if the write misses the selected record. - An [image table](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) T# whose surface has no mapped byte faults when selected instead of becoming a zero texture as a direct T# does; what the console returns for it is not measured. - [User-data callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is a user-data pair, or a copy of one made with `s_mov_b32` or `s_mov_b64` in the same block, calls the code at the address that user data holds. At dispatch the callee is read from guest memory up to its `s_setpc_b64` on the link pair, one copy of it is appended per call after the caller's last instruction, and the call is rewritten to `s_call_b64` to that copy. The words after the caller's last instruction are dropped from the recompiled code; in the shaders seen they are metadata or constants the caller reads relative to its program counter, and those reads go to the captured guest code, which linking does not change. A caller that jumps through a table among those words, which the control flow analysis reads from the recompiled code, throws. A copy runs at another program counter than the callee, so a callee that reads its program counter, makes a call, ends the program, jumps through another pair, returns before code it branches to, spans more than 64 KiB or is not readable throws. Callees are read again whenever a dispatch misses the dispatch cache; a dispatch that hits it reuses the linked program, so a title that rewrote a callee in place at the same address would keep running the old code. -- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block, calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. +- [Table callees](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a compute shader's `s_swappc_b64` whose source pair is two consecutive dwords of an `s_buffer_load_dword*`, or a copy of them made with `s_mov_b32` or `s_mov_b64` in the same block (a conditional branch forward past the call between them is crossed), calls one of the targets that buffer holds when the load's buffer descriptor comes from an `s_load_dwordx4` without SOFFSET from a user-data pointer, or from a pointer pair that two `s_mov_b32` of a literal or inline constant set, and the load's SOFFSET from an `s_lshl_b32` by 2 to 24 bits. At dispatch the descriptor and the buffer are read from guest memory, and every non-zero 64-bit value at the loaded offset plus a multiple of the shift's stride, below the descriptor's size (stride times num_records, or num_records bytes with stride 0), is read as a user-data callee and appended once per call. The call becomes a branch to a chain that compares the source pair with each target (`s_cmp_lg_u32` with a literal and `s_cbranch_scc1` per dword) and `s_call_b64`s the copy that matches; no match runs the original `s_swappc_b64` as an unlinked call. There is one chain for each value SCC has at the call, and the chain sets SCC back to that value (`s_cmp_eq_u32 0, 0` or `s_cmp_lg_u32 0, 0`) before the callee or the unlinked call runs. The searched range is the descriptor's whole size, which covers the range the load can reach whether it is checked in bytes or in strides; which one the PS5 uses for `s_buffer_load` was not measured. More than 64 distinct targets, a buffer above 64 MiB or a copy out of `s_call_b64` range throws. As with user-data callees, the buffer is read again only when a dispatch misses the dispatch cache, so a target a title adds to the buffer later is an unlinked call when a cached program runs. +- [AGC FS table](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Lifecycle.cpp) (libSceAgcDriver) - the driver maps 2 MiB at 0xF_E0000000 when it is created and fills the table that libSceAgc keeps at 0xF_E0040000 (six 16-byte V# slots, slot 0 describing eight 16-byte records) the way PS5 12.00 libSceAgcDriver and libSceAgc do, but every record points at one AnyPS5 routine that only returns (`s_setpc_b64` on the link pair). Sony's routines, which order work before a title's `ds_ordered_count`, are not modeled, and the other five slots stay zero. - [DB_RENDER_CONTROL resummarize draws](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - HTILE is not modeled, so a RESUMMARIZE_ENABLE draw without depth or stencil tests and without color writes changes no modeled state and draws nothing. A pure resummarize with pass-through D16/D32 depth, no depth writes or bounds test, no stencil plane, no color writes and no running pixel stage also draws nothing; other resummarize, copy or decompress draws throw. - [Comparison-sampled R32 float and R16 unorm textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) whose surface a storage image holds are sampled from a D32_SFLOAT or D16_UNORM image that the GPU refills from the storage image whenever its version moves, because Vulkan compares only depth formats. Only surfaces of at least 1 MiB (`APS5_DEPTH_COPY_MIN_KIB`) that are not 3D take the copy; smaller ones stay CPU snapshots compared against guest memory. With copies of small surfaces, one title hit an intermittent boot-time crash that was not explained. - [Null vertex buffer V#](../../../core/libs/prx/libSceAgcDriver/Graphics/include/VertexInput.hpp) (libSceAgcDriver) - a V# whose four dwords are all zero is bound to a zeroed vertex buffer of the attribute's size with stride 0, so every component of its attribute reads 0. This follows the V# fields in the [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) (`NUM_RECORDS` 0, `DST_SEL` 0 selects 0), not measured on hardware. From 78cabf5dcdc3845c8fb4c8dec7ecf3ce0537f734 Mon Sep 17 00:00:00 2001 From: Muhammad Wasif Ali Wasif <76695420+wachi35676@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:21:17 +0500 Subject: [PATCH 076/131] fix(libc): keep a drive-letter component of a guest path under its mount on Windows A guest path is resolved by appending its components to the host root or to a mount alias directory through std::filesystem::path. On Windows a component such as "C:" that came first in the appended part was parsed as a root name, so operator/ replaced the host directory instead of appending: "/savedata0/C:/Windows/win.ini" and "/app0/../C:/Windows/win.ini" resolved to "C:\Windows\win.ini", and "/C:/x" lost the component and resolved to "\x". A colon is an ordinary character in a FreeBSD file name, so those names belong under the mount. Guest paths are now split into component names and joined to the host directory as text. Explicit native drive paths are still passed through. On NTFS a name containing ":" still opens an alternate data stream; the libc debt list now records it. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/CMakeLists.txt | 10 +++ core/libs/prx/libc/src/General.cpp | 86 ++++++++++++++------- core/libs/tests/GuestPathDriveComponent.cpp | 51 ++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 core/libs/tests/GuestPathDriveComponent.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 4900ae6797..c5b0240cc3 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -321,6 +321,16 @@ if(BUILD_TESTING) set_tests_properties(guest_path_case PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_path_drive_component_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathDriveComponent.cpp) + target_include_directories(guest_path_drive_component_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_path_drive_component_tests PRIVATE libc) + configure_windows_unwind(guest_path_drive_component_tests) + add_test(NAME guest_path_drive_component COMMAND guest_path_drive_component_tests) + set_tests_properties(guest_path_drive_component PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_path_drive_component PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_directory_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectory.cpp) target_include_directories(guest_directory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_tests PRIVATE libc) diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 0f18419dfd..c9781df447 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -111,12 +111,39 @@ std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& path) { return RelinkedModulePath_nid_no_patch(path); } -std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::filesystem::path& relative, bool module = false) { - const auto direct = root / relative; +std::vector SplitNames(std::string_view text) { + std::vector names; + while (!text.empty()) { + const auto separator = text.find('/'); + names.emplace_back(text.substr(0, separator)); + if (separator == std::string_view::npos) break; + text.remove_prefix(separator + 1); + if (text.empty()) names.emplace_back(); + } + return names; +} + +std::string JoinNames(const std::vector& names) { + std::string joined; + for (std::size_t i = 0; i < names.size(); ++i) { + if (i != 0) joined += '/'; + joined += names[i]; + } + return joined; +} + +std::filesystem::path Child(std::filesystem::path parent, const std::string& name) { + parent /= ""; + parent += name; + return parent; +} + +std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::vector& relative, bool module = false) { + const auto direct = Child(root, JoinNames(relative)); std::error_code error; if (std::filesystem::exists(direct, error)) return direct; for (auto part = relative.begin(); part != relative.end(); ++part) { - const auto candidate = root / *part; + const auto candidate = Child(root, *part); const auto status = std::filesystem::symlink_status(candidate, error); if (std::filesystem::exists(status) || (error && error != std::errc::no_such_file_or_directory)) { root = candidate; @@ -124,7 +151,7 @@ std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::fil } const bool last = module && std::next(part) == relative.end(); const auto relinked = RelinkedModuleName(candidate); - NameMatch names[] = {{part->string()}, {relinked.filename().string()}}; + NameMatch names[] = {{*part}, {relinked.filename().string()}}; error = MatchNames(root, std::span(names, last ? 2 : 1)); if (names[0].ambiguous) throw std::runtime_error("Ambiguous case-insensitive guest path: " + candidate.string()); const bool matched = !error && names[0].path; @@ -152,12 +179,12 @@ std::optional ResolveAlias(const std::string& guestPath, } #ifndef _WIN32 if (guestPath.back() == '/') { - auto result = ResolveHostPath(host, relative.substr(prefix.size() + 1)); + auto result = ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1))); result /= ""; return module ? PathOrRelinkedModule(result) : result; } #endif - return ResolveHostPath(host, relative.substr(prefix.size() + 1), module).make_preferred(); + return ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1)), module).make_preferred(); } return std::nullopt; } @@ -171,8 +198,8 @@ WorkingDirectory& Directories() { static auto* state = new WorkingDirectory; return *state; } -std::optional ProvidedPath(const WorkingDirectory& state, const std::filesystem::path& guest) { - const auto relative = TrimSlashes(guest.relative_path().generic_string().c_str()); +std::optional ProvidedPath(const WorkingDirectory& state, const std::vector& names) { + const auto relative = TrimSlashes(JoinNames(names).c_str()); PathProvider provide = nullptr; { auto& aliases = Aliases(); @@ -182,20 +209,20 @@ std::optional ProvidedPath(const WorkingDirectory& state, } } if (provide == nullptr) return std::nullopt; - const auto supplied = ResolveHostPath(state.root, guest.relative_path()); + const auto supplied = ResolveHostPath(state.root, names); std::error_code error; if (std::filesystem::exists(supplied, error) || error) return supplied; auto provided = provide(state.root); if (provided.empty()) return std::nullopt; return provided.make_preferred(); } -std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest, bool module = false) { - if (auto aliased = ResolveAlias(guest.relative_path().generic_string(), module)) return *aliased; - if (auto provided = ProvidedPath(state, guest)) return *provided; - return ResolveHostPath(state.root, guest.relative_path(), module).make_preferred(); +std::filesystem::path HostPath(WorkingDirectory& state, const std::vector& names, bool module = false) { + if (auto aliased = ResolveAlias(JoinNames(names), module)) return *aliased; + if (auto provided = ProvidedPath(state, names)) return *provided; + return ResolveHostPath(state.root, names, module).make_preferred(); } -void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { - const auto host = HostPath(state, guest); +void RequireHostDirectory(WorkingDirectory& state, const std::vector& names) { + const auto host = HostPath(state, names); std::error_code error; const auto status = std::filesystem::status(host, error); if (std::filesystem::is_directory(status)) return; @@ -213,25 +240,32 @@ void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& #endif throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error); } +std::vector WithTrailingSeparator(std::vector names, bool trailing) { + if (trailing && !names.empty()) names.emplace_back(); + return names; +} std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false, bool archived = false) { std::string text(path); for (auto& character : text) if (character == '\\') character = '/'; - std::filesystem::path input(text); #ifdef _WIN32 // Preserve the existing ability to pass explicit native drive paths. - if (input.has_root_name()) return module ? PathOrRelinkedModule(input) : input; + if (std::filesystem::path input(text); input.has_root_name()) return module ? PathOrRelinkedModule(input) : input; #endif - const auto current = std::filesystem::path("/") / state.current.lexically_relative(state.root); - const auto relative = (input.is_absolute() ? input : current / input).relative_path(); - std::filesystem::path guest("/"); - for (auto part = relative.begin(); part != relative.end(); ++part) { - if (*part == "..") { - const auto parent = guest.lexically_normal(); - if (parent.has_relative_path() && !(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, parent)))) RequireHostDirectory(state, parent); + if (text.empty() || text.front() != '/') text = state.current.lexically_relative(state.root).generic_string() + "/" + text; + std::vector names; + bool trailing = true; + const auto parts = SplitNames(text); + for (std::size_t i = 0; i < parts.size(); ++i) { + const auto& name = parts[i]; + if (name.empty() && i + 1 != parts.size()) continue; + if (name == ".." && !names.empty()) { + if (!(archived && AppArchiveHasDirectory_nid_no_patch(HostPath(state, names)))) RequireHostDirectory(state, WithTrailingSeparator(names, trailing)); + names.pop_back(); } - guest /= *part; + trailing = name.empty() || name == "." || name == ".."; + if (!trailing) names.push_back(name); } - return HostPath(state, guest.lexically_normal(), module); + return HostPath(state, WithTrailingSeparator(std::move(names), trailing), module); } #ifdef _WIN32 using WriteMark = FILETIME; diff --git a/core/libs/tests/GuestPathDriveComponent.cpp b/core/libs/tests/GuestPathDriveComponent.cpp new file mode 100644 index 0000000000..c17bafcbfc --- /dev/null +++ b/core/libs/tests/GuestPathDriveComponent.cpp @@ -0,0 +1,51 @@ +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include + +static void Require(bool value) { if (!value) std::abort(); } + +static std::string Under(const std::filesystem::path& directory, const std::string& names) { + auto text = directory.string(); + for (std::size_t start = 0; start <= names.size();) { + const auto end = std::min(names.find('/', start), names.size()); + text += static_cast(std::filesystem::path::preferred_separator); + text += names.substr(start, end - start); + start = end + 1; + } + return text; +} + +int main() { + const auto host = std::filesystem::canonical(std::filesystem::current_path()); + const auto name = "AnyPS5-Drive-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const auto directory = host / name; + const auto mounted = directory / "Mounted"; + Require(std::filesystem::create_directories(mounted / "Nested")); + { std::ofstream file(directory / "Secret.txt"); file << 'x'; } + auto drive = host.root_name().string(); + if (drive.empty()) drive = "C:"; + std::string outside; + for (const auto& part : directory.relative_path()) outside += "/" + part.string(); + AddPathAlias_nid_no_patch("/drive-mount", mounted.string().c_str()); + + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive + outside + "/Secret.txt").c_str()).string() == + Under(mounted, drive + outside + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive + "Nested").c_str()).string() == Under(mounted, drive + "Nested")); + Require(ResolvePath_nid_no_patch(("/drive-mount/" + drive).c_str()).string() == Under(mounted, drive)); + Require(ResolvePath_nid_no_patch(("/drive-mount/Nested/" + drive + "/x").c_str()).string() == Under(mounted, "Nested/" + drive + "/x")); + Require(ResolvePath_nid_no_patch(("/" + name + "/../" + drive + outside + "/Secret.txt").c_str()).string() == + Under(host, drive + outside + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/" + drive + "/" + name + "/Secret.txt").c_str()).string() == + Under(host, drive + "/" + name + "/Secret.txt")); + Require(ResolvePath_nid_no_patch(("/" + drive).c_str()).string() == Under(host, drive)); + Require(ResolvePath_nid_no_patch(("/" + name + "/" + drive + "/x").c_str()).string() == Under(directory, drive + "/x")); + Require(ResolvePath_nid_no_patch(("/" + name + "/Secret.txt").c_str()) == directory / "Secret.txt"); + Require(ResolvePath_nid_no_patch("/drive-mount/Nested").string() == Under(mounted, "Nested")); + + RemovePathAlias_nid_no_patch("/drive-mount"); + std::filesystem::remove_all(directory); +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 79fa0585ce..af6fb19e5c 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -71,6 +71,7 @@ - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [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` +- [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 - [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. `gmtime` converts every time whose year fits an `int` on both hosts. From 88642b874fe461b5eebf9e3617074131efa3fb9f Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 13:05:25 -0400 Subject: [PATCH 077/131] fix(shader): declare the 64-bit integer type only for programs with BDA anchors EmitBdaAnchors declared the u64 type before checking for anchors, so every program got a 64-bit integer type, and programs without the Int64 capability failed SPIR-V validation. It now returns before declaring anything when the program has no anchors. A compute program without BDA reads is checked to declare no 64-bit integer type and no Int64 capability. AI-assisted: yes --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 23 +++++++++++++++++++ .../SpirvBackend/src/SpirvBdaRead.cpp | 1 + 2 files changed, 24 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 53f3512c90..d37165196e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -3737,6 +3737,29 @@ void verifyBdaAnchors() { require(directLoads == (anchored ? loads : 0u), "BDA anchors: an anchored read does not load its dwords directly"); } } + RecompileRequest plain{}; + const std::vector endOnly{0xbf810000u}; + plain.shader = {ShaderStage::Compute, 0x60000u, endOnly, 0, {}}; + plain.context.waveSize = 32; + plain.context.userDataBaseRegister = 0; + plain.context.userData = userData; + plain.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + plain.target.vulkanVersion = 0x00401000u; + plain.target.spirvVersion = 0x00010300u; + plain.target.subgroupSize = 32; + plain.target.bdaAbiVersion = BdaAbi::Version; + plain.target.fragmentShaderBarycentricEnabled = false; + plain.layout.pushConstantSizeBytes = 128; + plain.useCache = false; + const auto plainWords = Recompile(plain).spirv; + for (std::size_t cursor = 5; cursor < plainWords.size();) { + const auto length = plainWords[cursor] >> 16u; + require(length != 0 && length <= plainWords.size() - cursor, "BDA anchors: truncated SPIR-V instruction"); + const auto op = plainWords[cursor] & 0xffffu; + require(!(op == spv::OpTypeInt && plainWords[cursor + 2] == 64u), "BDA anchors: a program without anchors declares a 64-bit integer type"); + require(!(op == spv::OpCapability && plainWords[cursor + 1] == spv::CapabilityInt64), "BDA anchors: a program without anchors requires Int64"); + cursor += length; + } } void verifyFunctionLdsBound() { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index 2237fa9714..c17210edfc 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -541,6 +541,7 @@ void EmitBdaAnchorRange(SpirvEmitterState& state, SpirvEmitterState::BdaAnchor& } void EmitBdaAnchors(SpirvEmitterState& state) { + if (state.bdaAnchors.empty()) return; const auto u64 = TypeScalarU64(state); for (auto& anchor : state.bdaAnchors) { const auto low = Unary(state, spv::OpUConvert, u64, EmitGetUserData(state, static_cast(anchor.lowRegister))); From c732321bd3ea18a27680da5c5c1a5b46abbcf021 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Thu, 15 Oct 2026 00:48:06 +0000 Subject: [PATCH 078/131] docs(dev): describe the guest clock scale switch --- docs/dev/BUILD.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 03e4c72be7..8e1856f32f 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -203,6 +203,10 @@ A class range covers a command together with its own barriers. In `VulkanDevice: - `shadow-publish`: copies of unit shadow slabs into the import, with their barriers (`recordCopies` in `UnitShadow.cpp`, reached from `PublishShadow`). Copied bytes. - `template-refresh`: the `vkCmdUpdateBuffer` updates of a cached dispatch's changed data words, as one range from the first update to the last (`ShaderResources::refreshData`, called from `recordDispatch`). Refreshed bytes. +## Guest clock scale + +Set `APS5_TIME_SCALE=` to scale the guest process monotonic clocks and timestamp counter (TSC) from their first observed value. A positive factor below 1 slows guest time; a factor above 1 speeds it up. An unset, zero, negative or non-numeric value uses `1.0` (normal speed). Host sleeps and waits still take real time, so this is a diagnostic aid for titles whose frame or timeout logic depends on guest time; it does not speed up the emulator or GPU work. The setting is read once when the process starts. + ## Driver thread priority Set `APS5_THREAD_PRIORITY=high` to raise the AGC driver's queue workers and the video output presenter above the title's threads. On Linux the threads get nice `APS5_THREAD_NICE` (default -10) through rtkit; on Windows they get `THREAD_PRIORITY_HIGHEST`. `APS5_THREAD_PRIORITY=rt` asks rtkit for real-time priority `APS5_THREAD_RT` (default 10, at most rtkit's `MaxRealtimePriority`) under rtkit's policy, `SCHED_RR` by default; on Windows it sets `THREAD_PRIORITY_TIME_CRITICAL`. rtkit only grants it when the process's hard `RLIMIT_RTTIME` is at most its `RTTimeUSecMax` (200000 us by default), so a hard limit above `APS5_THREAD_RTTIME_US` (default 200000, capped at `RTTimeUSecMax`) is lowered to it and the soft limit to at most half of it. The hard limit cannot be raised again and stays lowered for the whole process even if rtkit then refuses; see [technical debt](TechnicalDebt.md#functional). There is no fallback between the modes: when rtkit or `SetThreadPriority` refuses, a queue worker reports the refusal as a driver failure, which the next submission, wait or shutdown rethrows, and the presenter's start throws it. Each raised thread logs its priority once as `[thread-priority]`. The setting is `off` by default; a value other than `off`, `high` or `rt` is reported the same way as a refusal. From a2ed4abead54662cf752899f6328341008a76b5c Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sat, 17 Oct 2026 03:26:09 +0000 Subject: [PATCH 079/131] docs(dev): clarify guest clock scale coverage --- docs/dev/BUILD.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 8e1856f32f..33f124a24d 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -205,7 +205,9 @@ A class range covers a command together with its own barriers. In `VulkanDevice: ## Guest clock scale -Set `APS5_TIME_SCALE=` to scale the guest process monotonic clocks and timestamp counter (TSC) from their first observed value. A positive factor below 1 slows guest time; a factor above 1 speeds it up. An unset, zero, negative or non-numeric value uses `1.0` (normal speed). Host sleeps and waits still take real time, so this is a diagnostic aid for titles whose frame or timeout logic depends on guest time; it does not speed up the emulator or GPU work. The setting is read once when the process starts. +Set `APS5_TIME_SCALE=` to scale `sceKernelGetProcessTime`, `sceKernelGetProcessTimeCounter` and `sceKernelReadTsc` from their first observed value. A positive factor below 1 slows guest time; a factor above 1 speeds it up. An unset, zero, negative or non-numeric value uses `1.0` (normal speed). On Windows, `sceKernelClockGettime` monotonic clock ids are scaled too; on Linux they return the native host monotonic clock unchanged. A title's own `rdtsc` instruction and `sceKernelGetTscFrequency` are unscaled, so dividing scaled TSC deltas by the reported real frequency gives guest-time intervals. Host sleeps and waits still take real time, so this is a diagnostic aid for titles whose frame or timeout logic depends on guest time; it does not speed up the emulator or GPU work. The setting is read once when the process starts. + +`APS5_PAD_INPUT_FILE` command durations and reload polling use `sceKernelGetProcessTime`, so they follow the scale. At `0.02`, a 1-second injected press lasts about 50 seconds of real time, and a new command can take up to 5 seconds to be noticed. ## Driver thread priority From 8b81fa09bfded6ad483ccad41e25721a44d008d2 Mon Sep 17 00:00:00 2001 From: milescoviello Date: Wed, 14 Oct 2026 23:09:59 +0000 Subject: [PATCH 080/131] fix(libSceNet): close a libkernel socket through sceNetSocketClose MXGP 26 (PPSA30839) opens TCP sockets with libkernel's socket() and later closes them with sceNetSocketClose. libSceNet only looked in its own socket table, so every one of those closes failed with SCE_NET_ERROR_EBADF and the host socket stayed open. On the console both calls work on the same kernel sockets. A descriptor libSceNet does not own but libkernel has open as a socket is now closed through libkernel's close(). Anything else still fails with EBADF. The fallback runs after g_mutex is released, because libkernel's close() first asks libSceNet's socket closer, which takes it. guest_sce_net closes a libkernel TCP socket with sceNetSocketClose, checks that libkernel no longer has it open, and checks that a second close fails with EBADF. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceNet/Export.cpp | 16 +++++++++++++--- core/libs/tests/GuestSceNet.cpp | 10 ++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 8fd8578762..f7940e7938 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -36,6 +36,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" +#include "prx/libkernel/Socket/include/SocketRuntime.hpp" // Guest socket descriptors retain PS5 semantics while their transport is backed by host sockets. @@ -763,14 +764,23 @@ int APS5_VABI sceNetSocket(const char* name, int family, int type, int protocol) return fd; } +extern "C" int APS5_VABI close_nid_postfix(int d); + int APS5_VABI sceNetSocketClose(int s) { std::shared_ptr native; + bool found = false; { std::lock_guard lk(g_mutex); const auto it = g_socks.find(s); - if (it == g_socks.end()) return fail(NET_EBADF); - native = it->second.native; - g_socks.erase(it); + if (it != g_socks.end()) { + native = it->second.native; + g_socks.erase(it); + found = true; + } + } + if (!found) { + if (!GuestSocketIsOpen_nid_no_patch(s)) return fail(NET_EBADF); + return close_nid_postfix(s) == 0 ? 0 : fail(*__error_nid_postfix()); } KernelReleaseSocketNumber_nid_no_patch(s); if (native) { diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index e5b47cdeae..20ba9c8e57 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -76,6 +76,7 @@ int APS5_VABI open_nid_postfix(const char*, int, int); int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI bind_nid_postfix(int, const void*, std::uint32_t); int APS5_VABI close_nid_postfix(int); +bool GuestSocketIsOpen_nid_no_patch(int); int APS5_VABI setsockopt_nid_postfix(int, int, int, const void*, std::uint32_t); int APS5_VABI fcntl_nid_postfix(int, int, ...); struct GuestPollEntry { @@ -991,6 +992,14 @@ static void CheckSocketError() { Require(sceNetSocketClose(refused) == 0); } +static void CheckCloseKernelSocket() { + const int kernel = socket_nid_postfix(2, 1, 0); + Require(kernel >= 0 && GuestSocketIsOpen_nid_no_patch(kernel)); + Require(sceNetSocketClose(kernel) == 0); + Require(!GuestSocketIsOpen_nid_no_patch(kernel)); + Require(Failed(sceNetSocketClose(kernel), 9)); +} + static void CheckDatagramDisconnect() { const int peer = sceNetSocket("disconnect-peer", 2, 2, 17); const int other = sceNetSocket("disconnect-other", 2, 2, 17); @@ -1060,6 +1069,7 @@ int main() { CheckUnspecifiedIpv6(); if (!ipv6Unavailable) CheckIpv6SocketsAreIpv6Only(); CheckDatagramDisconnect(); + CheckCloseKernelSocket(); const int closed = sceNetSocket("closed-by-kernel", 2, 2, 17); Require(closed >= 0); From bf47f840efef6792a16a189165ed32f79ed5fb65 Mon Sep 17 00:00:00 2001 From: ivica00 Date: Sun, 18 Oct 2026 00:14:02 +0200 Subject: [PATCH 081/131] fix(agc): keep pipelined DMA opt-in and order copies against queued writes Co-authored-by: Cursor --- .../src/Driver/Packets/PacketExecution.cpp | 4 +++- .../src/Driver/Packets/PacketMemory.cpp | 2 +- .../tests/execution/PipelinedDma.cpp | 17 ++++++++++++++--- docs/dev/ARCHITECTURE.md | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) 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 bd6f0050da..52d75889a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -38,12 +38,14 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head if (destination == Pm4::DmaSelectGds || destination == Pm4::DmaDestinationNowhere) return true; const auto bytes = Pm4::DmaBytes(packet); if (bytes == 0) return true; + const auto source = Pm4::DmaSource(packet); + if ((source == 0 || source == 3) && DrawPipeline::Queue0().Overlaps(packet[2] | (static_cast(packet[3]) << 32u), bytes)) return false; const auto address = packet[4] | (static_cast(packet[5]) << 32u); return !DrawPipeline::Queue0().Overlaps(address, bytes); } case 0x10: { const auto operation = (header >> 2u) & 0x3fu; - return operation != 0x09; + return operation == 0 || operation == 0x09 || operation == 0x0b || operation == 0x0c || operation == 0x1a; } default: return false; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index a7d985279d..57187bf8ef 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -268,7 +268,7 @@ void Driver::dumpSampleCounters(std::uint64_t address) { } bool Driver::enqueueDmaPacket(std::span packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state) { - static const bool enabled = std::getenv("APS5_NO_PIPELINE_DMA") == nullptr; + static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; if (!enabled || opcode != 0x50 || queue != 0 || packet.size() < 7 || !DrawPipeline::Active() || !deferredLabels().labels.empty()) return false; const auto source = ((packet[1] >> 29u) & 3u) | ((packet[6] >> 24u) & 4u) | ((packet[6] >> 25u) & 8u); constexpr std::size_t immediateLimit = std::size_t{16} << 20u; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp index 71a77737d5..d43e135c35 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/PipelinedDma.cpp @@ -74,10 +74,10 @@ void ExpectNoDrain(std::uint32_t opcode, std::uint64_t before, const char* what) int main() { #ifdef _WIN32 _putenv_s("APS5_PIPELINED_DRAWS", "1"); - _putenv_s("APS5_NO_PIPELINE_DMA", ""); + _putenv_s("APS5_PIPELINE_DMA", "1"); #else setenv("APS5_PIPELINED_DRAWS", "1", 1); - unsetenv("APS5_NO_PIPELINE_DMA"); + setenv("APS5_PIPELINE_DMA", "1", 1); #endif try { alignas(256) static std::array first{}; @@ -137,6 +137,15 @@ int main() { } AgcDriverWaitIdle_nid_postfix(); + { + Gate gate; + Hold(gate, held, sizeof(pending)); + words.clear(); + Append(words, DmaData(false, held, 0, other, 2)); + ExpectDrains(words, 0x50, gate, "an unaligned copy of an uncommitted pipeline write did not drain"); + } + AgcDriverWaitIdle_nid_postfix(); + { Gate gate; Hold(gate, held, sizeof(pending)); @@ -150,9 +159,11 @@ int main() { { Gate gate; Hold(gate, held, sizeof(pending)); + const auto resetBefore = DrawPipeline::DrainRequestsByOpcode(0x10); words.clear(); Append(words, std::array{0xc0001024u, 0u}); - ExpectDrains(words, 0x10, gate, "DISPATCH_RESET did not drain the pipeline"); + Submit(words); + ExpectNoDrain(0x10, resetBefore, "DISPATCH_RESET drained the pipeline"); } LibcRunShutdown_nid_postfix(); diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 35642e7b88..3833a11d11 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -98,7 +98,7 @@ flowchart LR - The Vulkan pipelines are journaled as well ([`PipelineJournal.cpp`](../../core/libs/prx/libSceAgcDriver/Graphics/src/PipelineJournal.cpp)): a graphics pipeline the store builds (`CachedPipeline`) or a compute pipeline a dispatch builds is appended to `pipelines.journal` next to the shader cache entries, as the materialized result of each stage (the specialized SPIR-V, bindings and push constants, through `ShaderDiskCache::EncodeResult`) plus the recipe the pipeline was built from (`GraphicsPipelineRecipe`: the stages' identities, vertex input layout, descriptor set layout keys and the state fields the `Pipeline` constructor reads, `CanonicalPipelineState`). When a `VulkanDevice` is created, the journal is loaded and its pipelines are built on a pool of `hardware_concurrency() / 2` threads (`APS5_PIPELINE_JOURNAL_THREADS`) while the title loads, so the first draw or dispatch of a configuration seen on an earlier run finds its pipeline resident. The pipeline stores key on `RecompileResult::Identity`, a hash of the module words, specialization constants, subgroup width and bindings that is the same on every run (`SharedSpirv::ContentHash`, computed once per shared module), rather than on the process-lifetime variant ids, and a graphics pipeline is built outside the store's mutex, so the replay threads and the queue workers build in parallel. A draw that arrives before its replayed entry builds the pipeline itself; the first inserted wins. A replayed entry enters the store at its least recently used end and a full store takes none, so the title's own pipelines are never evicted for a replayed one, and the optimized relink of a replayed pipeline (`OptimizedPipeline::Schedule`) waits for the first draw that binds it, so the relink thread serves the title's pipelines first. A device created without a window starts its replay at its first draw or dispatch rather than at creation, since `Driver::AttachWindow` usually replaces it before the title draws. The journal records a pipeline once (modules and recipes are deduplicated by content), keeps at most `APS5_PIPELINE_JOURNAL_MAX_MIB` (1024), replays the first `APS5_PIPELINE_JOURNAL_ENTRIES` recorded (1024, the pipeline store's bound, `APS5_PIPELINE_STORE_ENTRIES`; a run reaches those first), and is disabled with `APS5_NO_PIPELINE_JOURNAL=1` or without a shader cache directory. The file header names the device (`PipelineJournalDeviceFingerprint`, the properties the driver's pipeline cache file is named by), so a journal written for another GPU or driver is started over rather than replayed; appends and reads hold `pipelines.journal.lock` against another process sharing the directory, and a device cancels its replay before it is replaced (`PrepareForReplacement`) or destroyed, so a replaced headless device never inserts entries the window's device then sweeps. `APS5_PROFILE_DRAW=1` prints a `[pipeline-journal]` summary when the replay finishes and at teardown. - A compute shader whose LDS plus its lock dword exceeds the host's `maxComputeSharedMemorySize` is not given a `Workgroup` array. `Recompile` sets `ShaderInfo::sharedMemoryBytes` for it and the SPIR-V backend addresses the LDS in a coherent storage buffer at descriptor set 1 (`WorkgroupMemoryDescriptorSet`), with a slice of `RecompileResult::workgroupMemoryDwords` per workgroup, indexed by `WorkgroupID` and `NumWorkGroups`. `VulkanDevice` keeps one such buffer, sized from the dispatch dimensions, and binds it after the resource set; programs that fit keep using shared memory. - `Driver::RegisterShader` splits shader preparation in two. `PlanRegistered` runs on the registering guest thread: it validates the registers and entry point, decodes the code and builds the recompile request, so invalid registrations still fail there. The RDNA to IR translation and resource plan (`PrepareShader`) run on a background pool of `hardware_concurrency() / 2` detached threads (at least 2, 64 MiB stacks). -- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and DMA packets are queued as items instead (`APS5_NO_PIPELINE_DMA` leaves a DMA packet on the worker). With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. +- Queue 0's packet worker decodes, captures and prepares a draw and hands its recording (`Graphics::Draw` under the GPU lock) to a committer thread in [`DrawPipeline.cpp`](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp), so the next packet is decoded while the previous draw is recorded. Each queued item carries the guest ranges it will write (`DrawWriteRanges`: written buffers and storage images, color targets with their DCC and CMASK, depth and stencil). A later packet that reads or writes one of those ranges, or that the pipeline can't order, drains it first; labels, submission ends, buffer fills and (with `APS5_PIPELINE_DMA=1`) DMA packets are queued as items instead. With `APS5_PIPELINE_DISPATCH=1` (off by default), a `DISPATCH_DIRECT` whose dispatch-cache entry hits with a recipe is queued as an item too, carrying the dispatch's written buffers and storage images (`DispatchWriteRanges`); the capture and the cache lookup stay on the worker and the recipe is recorded on the committer. A dispatch still drains when its lookup misses, when what it reads overlaps a write still pending, when the hit is verified against data or when its writes cannot be ranged (bindless access, GDS, fault buffer). An exception thrown by a commit is rethrown on the worker at its next enqueue or drain, as an inline draw error would be. On by default; `APS5_PIPELINED_DRAWS=0` records every draw inline, and `APS5_EXACT_DRAW_WRITES=0` widens the color and depth ranges to estimates. - 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. From 9f000e861043d82d19390c67420e64671b80dcfb Mon Sep 17 00:00:00 2001 From: Jota <139649225+jotitadev@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:05:14 -0300 Subject: [PATCH 082/131] test(relinker): cover a PT_TLS inside an uncovered PT_SCE_RELRO --- core/relinker/relinker/tests/ElfReaderBoundsTests.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp index 1597cf6807..8b1c17d895 100644 --- a/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp +++ b/core/relinker/relinker/tests/ElfReaderBoundsTests.cpp @@ -142,6 +142,12 @@ int main() { Require(headers.size() == 2 && headers[1].Type == 7 && headers[1].MappedAddress == address); } }}, + {"TLS image inside uncovered SCE RELRO", [] { + auto bytes = TlsFixture(0x1008, 8, 16, 8); + Write(bytes, 0x260, 0x61000010); + const auto headers = Relinker::ElfReader(std::move(bytes)).ReadProgramHeaders(); + Require(headers.size() == 2 && headers[0].Type == 1 && headers[1].Type == 7 && headers[1].MappedAddress == 0x1008); + }}, {"TLS image outside load memory", [] { constexpr std::uint64_t addresses[] = {0x0ff8, 0x103c, 0x2000, std::numeric_limits::max() - 3}; for (const std::uint64_t address : addresses) { From af0af833c112bb1c0640cf752f4e2a6700420d2a Mon Sep 17 00:00:00 2001 From: Crimson <144462348+CrimsonMac@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:30:45 +0300 Subject: [PATCH 083/131] fix(shader): use 64-bit buffer atomics for device-memory LDS Compute LDS beyond the device's shared-memory limit lives in a storage buffer, and its 64-bit atomics took a lock in that buffer. AMD's Windows driver compiles the lock's acquire and release to s_barrier, which inside the divergent lock loop pairs with the program's own barriers, so the device-memory case of agc_driver_lds_lock_budget read half its sum. With shaderBufferInt64Atomics the device-memory LDS now uses a 64-bit view of the buffer instead: 64-bit atomics are compare-exchange loops on it and 32-bit atomics stay native, so no lock is taken. Without the feature the lock is kept. The per-workgroup slice is rounded up to an even dword count so that the 64-bit view stays aligned. --- .../libSceAgcDriver/tests/LdsLockBudget.cpp | 50 +++++++++++++------ .../tests/execution/LdsBeyondDeviceLimit.cpp | 4 +- .../tests/execution/LdsMixedWidthAtomics.cpp | 2 +- .../IrMetadata/ShaderInfo.hpp | 2 +- .../include/SpirvBackend/SpirvAnalysis.hpp | 1 + .../SpirvBackend/SpirvEmitterHelpers.hpp | 1 + .../SpirvBackend/SpirvEmitterState.hpp | 1 + .../SpirvMemory/SpirvBufferAccess.hpp | 1 + .../SpirvBackend/src/SpirvEmitter.cpp | 4 ++ .../src/SpirvMemory/SpirvBufferAccess.cpp | 9 ++++ .../src/SpirvMemory/SpirvEmitterHelpers.cpp | 8 +++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 12 ++++- .../src/SpirvMemory/SpirvModuleSetup.cpp | 4 ++ docs/dev/debt/libSceAgcDriver.md | 2 +- docs/dev/debt/shader.md | 2 +- 15 files changed, 82 insertions(+), 21 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp index 8901e04a35..e69551e405 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -56,12 +57,27 @@ std::uint64_t workgroupBytes(std::span words) { return bytes; } -RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { +std::set compareExchangeWidths(std::span words) { + std::map widths; + std::set found; + for (std::size_t offset = 5; offset < words.size();) { + const auto count = words[offset] >> 16u; + require(count != 0u && count <= words.size() - offset, "malformed SPIR-V instruction"); + const auto instruction = words.subspan(offset, count); + const auto opcode = static_cast(instruction[0] & 0xffffu); + if (opcode == spv::OpTypeInt) widths.emplace(instruction[1], instruction[2]); + if (opcode == spv::OpAtomicCompareExchange) found.insert(widths.at(instruction[1])); + offset += count; + } + return found; +} + +RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) { static constexpr std::array code{ 0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u, 0xd9000000u, 0x00000201u, 0xbf8cc07fu, 0xbf810000u }; - static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + static constexpr std::array capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess, spv::CapabilityInt64Atomics}; static constexpr std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; static constexpr std::array userData{}; RecompileRequest request{}; @@ -73,7 +89,7 @@ RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { request.target.spirvVersion = 0x00010600u; request.target.subgroupSize = 32; request.target.bdaAbiVersion = 1; - request.target.supportedCapabilities = capabilities; + request.target.supportedCapabilities = std::span(capabilities).first(int64Atomics ? 8u : 7u); request.target.supportedExtensions = extensions; request.target.maxWorkgroupSize = {1024u, 1024u, 64u}; request.target.maxWorkgroupInvocations = 1024; @@ -83,13 +99,17 @@ RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) { return Recompile(request); } -void verify(std::uint32_t waveSize, std::uint32_t limit) { - const auto result = compile(waveSize, limit); +void verify(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) { + const auto result = compile(waveSize, limit, int64Atomics); require(!result.spirv.empty(), "LDS atomic program did not compile"); const auto declared = workgroupBytes(result.spirv.Words()); require(declared <= limit, "declared " + std::to_string(declared) + " Workgroup bytes exceeds limit " + std::to_string(limit)); - require(result.workgroupMemoryDwords == (limit < 68u ? 17u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit)); + require(result.workgroupMemoryDwords == (limit < 68u ? 18u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit)); if (limit >= 68u) require(declared != 0u, "in-budget LDS unexpectedly lost its Workgroup allocation"); + const auto widths = compareExchangeWidths(result.spirv.Words()); + const bool native = int64Atomics && limit < 68u; + require(widths.contains(64u) == native, native ? "device-memory LDS did not use a 64-bit compare exchange" : "a 64-bit compare exchange appeared without device-memory LDS and shaderBufferInt64Atomics"); + require(widths.contains(32u) != native, native ? "device-memory LDS with shaderBufferInt64Atomics kept the workgroup lock" : "the 64-bit LDS atomic lost its workgroup lock"); } } @@ -98,14 +118,16 @@ int main() { unsigned failures = 0; for (const auto waveSize : {32u, 64u}) { for (const auto limit : {64u, 68u, 72u}) { - const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit); - try { - verify(waveSize, limit); - std::cout << "LDS lock budget: " << name << " passed\n"; - } catch (const std::exception& error) { - ++failures; - const std::string message(error.what()); - std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n'; + for (const bool int64Atomics : {false, true}) { + const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit) + (int64Atomics ? " int64 atomics" : ""); + try { + verify(waveSize, limit, int64Atomics); + std::cout << "LDS lock budget: " << name << " passed\n"; + } catch (const std::exception& error) { + ++failures; + const std::string message(error.what()); + std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n'; + } } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp index 8b37c4591f..fb6fd2d583 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsBeyondDeviceLimit.cpp @@ -148,7 +148,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRe Require(within.workgroupMemoryDwords == 0u, "lds beyond the device limit " + run + ": LDS below the device limit left workgroup memory"); fits(within, "LDS below the device limit"); const auto atLimit = Compile(waveSize, limit, target); - Require(atLimit.workgroupMemoryDwords == limit / 4u + 1u, "lds beyond the device limit " + run + ": LDS at the device limit gave " + std::to_string(atLimit.workgroupMemoryDwords) + " device memory dwords per workgroup"); + Require(atLimit.workgroupMemoryDwords == limit / 4u + 2u, "lds beyond the device limit " + run + ": LDS at the device limit gave " + std::to_string(atLimit.workgroupMemoryDwords) + " device memory dwords per workgroup"); fits(atLimit, "LDS at the device limit"); const auto bytes = limit + AboveLimitBytes; for (std::uint32_t tid = 0; tid < Threads; ++tid) { @@ -159,7 +159,7 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t waveSize, const ShaderRe } Output.fill(Untouched); const auto result = Compile(waveSize, bytes, target); - Require(result.workgroupMemoryDwords == bytes / 4u + 1u, "lds beyond the device limit " + run + ": " + std::to_string(bytes) + " bytes of LDS gave " + std::to_string(result.workgroupMemoryDwords) + " device memory dwords per workgroup"); + Require(result.workgroupMemoryDwords == bytes / 4u + 2u, "lds beyond the device limit " + run + ": " + std::to_string(bytes) + " bytes of LDS gave " + std::to_string(result.workgroupMemoryDwords) + " device memory dwords per workgroup"); fits(result, "LDS above the device limit"); if (indirect) { IndirectGroups = {groupsX, groupsY, groupsZ, 0u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp index 7c35530d9f..f63631ee61 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp @@ -253,7 +253,7 @@ void LockBudget(AgcDriver::VulkanDevice& device) { if (std::getenv("ANYPS5_REQUIRE_ROBUST2") != nullptr) Require(target.robustBufferAccess2, "lds lock budget: robustBufferAccess2 was not enabled"); Require(limit >= 32u && limit % 4u == 0u, "lds lock budget: unexpected device shared-memory limit"); const auto operations = static_cast(Threads) * Iterations; - Run(device, UniformCode, "limit", operations, 1, limit / 4u, limit / 4u + 1u); + Run(device, UniformCode, "limit", operations, 1, limit / 4u, (limit / 4u + 2u) & ~1u); Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, 0u); std::puts("lds lock budget tests passed"); } diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 8879b166be..1440effcb2 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -42,7 +42,7 @@ struct ShaderInfo { }; [[nodiscard]] inline std::uint32_t WorkgroupMemoryStrideDwords(const ShaderInfo& info) { - return info.sharedMemoryBytes == 0u ? 0u : info.sharedMemoryBytes / 4u + 1u; + return info.sharedMemoryBytes == 0u ? 0u : (info.sharedMemoryBytes / 4u + 2u) & ~1u; } } diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index 1b1c53242b..ebfd8888e3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -19,6 +19,7 @@ struct SpirvRequirements { bool imageGatherExtended = false; bool functionLds = false; bool ldsLock = false; + bool ldsBufferInt64Atomics = false; std::uint32_t functionLdsDwords = 0; std::unordered_map functionLdsAddresses; bool functionScratch = false; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 836b58a04c..e2101177cf 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -112,6 +112,7 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t rawIndex); std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index e293dd87c7..efafc0b8b3 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -164,6 +164,7 @@ struct SpirvEmitterState { std::uint32_t flattenedSrtVariable = 0; std::uint32_t ldsVariable = 0; std::uint32_t ldsBufferVariable = 0; + std::uint32_t ldsBufferU64Variable = 0; std::uint32_t ldsBufferBase = 0; std::uint32_t numWorkgroupsVariable = 0; std::array scratchVariable {}; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp index 03bd0330d6..53a851625b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp @@ -15,6 +15,7 @@ bool LdsInDeviceMemory(const SpirvEmitterState& state); std::uint32_t LdsMemorySemantics(const SpirvEmitterState& state); void EmitLdsBufferBase(SpirvEmitterState& state); std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state); +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index); MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem); std::uint32_t EmitMemoryElementIndex(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t rawIndex); std::uint32_t EmitMemoryElementInBounds(SpirvEmitterState& state, const MemoryResourceAccess& access, std::uint32_t index); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index f30656653d..050316a6f7 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -262,6 +262,10 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.float16DenormPreserve = target.float16DenormPreserve; state.robustBufferAccess2 = target.robustBufferAccess2; state.hostSubgroupSize = target.subgroupSize; + 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; + } if (HostSubgroupSingleLane(state)) { state.requirements.subgroupBallot = false; state.requirements.subgroupShuffle = false; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index d208290cac..192b60cc27 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -111,6 +111,15 @@ std::uint32_t EmitLdsLockPointer(SpirvEmitterState& state) { return pointer; } +std::uint32_t EmitLdsBufferU64Pointer(SpirvEmitterState& state, std::uint32_t index) { + if (!state.requirements.ldsBufferInt64Atomics || state.ldsBufferU64Variable == 0u || state.ldsBufferBase == 0u) { + FailEmit("64-bit device memory LDS atomics were not prepared at function entry"); + } + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, TypePointer(state, spv::StorageClassStorageBuffer, TypeScalarU64(state)), pointer, state.ldsBufferU64Variable, ConstantU32(state, 0u), EmitBinaryU32(state, spv::OpShiftRightLogical, EmitAddU32(state, state.ldsBufferBase, index), ConstantU32(state, 1u))); + return pointer; +} + MemoryResourceAccess PrepareMemoryResourceAccess(SpirvEmitterState& state, const MemoryInfo& mem) { MemoryResourceAccess access; access.kind = mem.kind; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index 2f44307dc3..7a704ea52d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -518,6 +518,14 @@ void DefineDescriptors(SpirvEmitterState& state) { state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationDescriptorSet, WorkgroupMemoryDescriptorSet); state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationBinding, 0u); state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferVariable, spv::DecorationCoherent); + if (state.requirements.ldsBufferInt64Atomics) { + state.ldsBufferU64Variable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassStorageBuffer, StorageBufferU64BlockType(state)), spv::StorageClassStorageBuffer); + state.module.AddName(state.ldsBufferU64Variable, "workgroup_memory_u64"); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationDescriptorSet, WorkgroupMemoryDescriptorSet); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationBinding, 0u); + state.module.AddAnnotation(spv::OpDecorate, state.ldsBufferU64Variable, spv::DecorationCoherent); + for (const auto variable : {state.ldsBufferVariable, state.ldsBufferU64Variable}) state.module.AddAnnotation(spv::OpDecorate, variable, spv::DecorationAliased); + } } } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index d561dea75c..417ce2c9de 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -761,6 +761,11 @@ std::uint32_t UpdateFunctionElement(SpirvEmitterState& state, std::uint32_t poin template std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, std::uint32_t type, std::uint32_t zero, TUpdate&& update); +template +std::uint32_t LdsBufferAtomic64(SpirvEmitterState& state, std::uint32_t low, TNext&& next) { + return Unary(state, spv::OpBitcast, TypeU64(state), AtomicUpdateTyped(state, EmitLdsBufferU64Pointer(state, low), ResourceKind::Lds, TypeScalarU64(state), next)); +} + template std::uint32_t EmitLdsAtomic32(SpirvEmitterState& state, const MemoryInfo& mem, TOperation&& operation) { if (mem.kind != ResourceKind::Lds || !state.requirements.ldsLock) return operation(); @@ -1145,7 +1150,7 @@ std::uint32_t ApertureAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, 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); }); if (kind == ResourceKind::Scratch && state.program.Info().scratchDwords == 0u) return ConstantU64(state, 0u); - if (kind == ResourceKind::Lds && !state.requirements.ldsLock) ctx.Fail(inst, "64-bit LDS atomic reached through a flat address without the workgroup lock"); + if (kind == ResourceKind::Lds && !state.requirements.ldsLock && !state.requirements.ldsBufferInt64Atomics) ctx.Fail(inst, "64-bit LDS atomic reached through a flat address without the workgroup lock"); MemoryInfo storage{}; storage.kind = kind; const auto resource = PrepareMemoryResourceAccess(state, storage); @@ -1155,6 +1160,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); }); const auto update = [&]() { const auto lowPointer = EmitMemoryElementPointer(state, resource, low); const auto highPointer = EmitMemoryElementPointer(state, resource, high); @@ -1387,6 +1393,10 @@ std::uint32_t SharedAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, TR const auto low = EmitMemoryElementIndex(state, resource, rawIndex); const auto high = EmitMemoryElementIndex(state, resource, Binary(state, spv::OpIAdd, TypeU32(state), rawIndex, ConstantU32(state, 1u))); return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&]() { + if (state.requirements.ldsBufferInt64Atomics) { + const auto aligned = Binary(state, spv::OpIEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), low, ConstantU32(state, 1u)), ConstantU32(state, 0u)); + return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&]() { return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return replacement(state, old); }); }); + } const auto update = [&]() { const auto lowPointer = EmitMemoryElementPointer(state, resource, low); const auto highPointer = EmitMemoryElementPointer(state, resource, high); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 763f1a1fa8..ef3e1ac42d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -265,6 +265,10 @@ void DefineModule(SpirvEmitterState& state) { if (state.requirements.sharedInt64Atomics) { state.module.EmitCapability(spv::CapabilityInt64); } + if (state.requirements.ldsBufferInt64Atomics) { + state.module.EmitCapability(spv::CapabilityInt64); + state.module.EmitCapability(spv::CapabilityInt64Atomics); + } if (state.requirements.imageInt64Atomics) { if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityInt64ImageEXT)) == state.supportedCapabilities.end()) { FailEmit("64-bit image atomics need VK_EXT_shader_image_atomic_int64"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index d456a047e5..4107aa6246 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -68,7 +68,7 @@ - [Unused pixel programs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): a bound pixel program is skipped when color masks have no intersection and DB export and execution enables are clear, regardless of retained SPI_SHADER_Z_FORMAT. This follows [AMD PAL DbShaderControl](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/gfx9AbiToPipelineRegisters.h) and its [GFX10 register definitions](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/hw/gfxip/gfx9/chip/gfx9_plus_merged_registers.h), not a hardware measurement. Enabled DB_ALPHA_TO_MASK conservatively retains the program even with ALPHA_TO_MASK_DISABLE, because DecodeState currently enables Vulkan alpha-to-coverage without applying that override. Console behavior for these combinations has not been measured. - [Sample masks](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `PA_SC_AA_MASK_X0Y0_X1Y0` and `PA_SC_AA_MASK_X0Y1_X1Y1` hold one 16-bit sample mask for each pixel of the aligned 2x2 quad ([Mesa gfx10.json](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/registers/gfx10.json#L13512)); only the bits of the `MSAA_NUM_SAMPLES` rasterized samples count, and at one sample bit 0 of a pixel's field covers every sample of the target the draw is raised to. A mask equal for the four pixels is the Vulkan `pSampleMask`, the case [radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13633) and [radeonsi](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/gallium/drivers/radeonsi/si_state.c#L4218) program; with early fragment tests it needs `earlyFragmentSampleMaskTestBeforeSampleCounting` ([radv](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_physical_device.c#L2079) reports it). A mask that differs between the pixels is applied in the pixel shader without early fragment tests: a pixel whose mask covers all or none of its samples is demoted to a helper invocation by `gl_FragCoord` parity, and otherwise the pixel's mask is ANDed into `gl_SampleMask` (with alpha-to-coverage and the mask export) after demoting invocations whose `SampleMask` input it leaves empty. A pixel program with early Z that writes memory under depth, stencil or depth-bounds tests is therefore refused. That XnYm is the pixel at (x mod 2, y mod 2) = (n, m) is read from the field names; that the mask removes samples before the depth and stencil tests and occlusion counting is inferred from radv implementing the Vulkan sample mask test with it; and that it takes no part in the centroid (`AA_MASK_CENTROID_DTMN` 0) is inferred from radv setting that bit only for conservative rasterization. None of it is measured on gfx1013. A mask equal for the four pixels that removes some but not all samples of a pixel is refused with active centroid inputs, since where Vulkan puts the centroid relative to `pSampleMask` is implementation-defined, and any partial mask is refused with conservative rasterization (radv sets `PREZ_AA_MASK_ENABLE` and `POSTZ_AA_MASK_ENABLE` [there](https://gitlab.freedesktop.org/mesa/mesa/-/blob/7b87f8ad226e/src/amd/vulkan/radv_cmd_buffer.c#L13653), which 0x6001 leaves clear). - [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 per workgroup (the last dword is the atomic lock), 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. +- [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. - [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 diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c567f99417..3f58b62d0a 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -129,7 +129,7 @@ - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 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. +- [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, 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. - [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. From ef3cb7c9ef3a893b52b73ef9cf0cd988f7724bb1 Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Sun, 18 Oct 2026 09:57:24 +0200 Subject: [PATCH 084/131] docs(agc): bound the released-texture sweep by the grown resource cache Co-Authored-By: Claude Opus 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 e67564161c..6c7838b646 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -172,7 +172,7 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. -- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (up to 1,024, each with its textures); that cost is not measured. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344) on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. +- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344) on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. From 241429c45cfed0265f51b76e4d47ec07180a01d1 Mon Sep 17 00:00:00 2001 From: Lberonio <103309975+Lberonio@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:09:53 +0300 Subject: [PATCH 085/131] fix(agc): clear a queue's flip event latch once its own interrupts stop A queue that delivered an end-of-pipe interrupt from its own release stopped delivering the flip's event for the rest of the run, so a title that raises a release interrupt once and then waits on the flip event never saw it again. Count the flips a queue has taken since its last delivered release interrupt: four consecutive flips keep the latch, a fifth clears it and raises the event, so the queue recovers within five flips. Gran Turismo 7 (PPSA01317) pairs two flips with one release interrupt per frame, which stays under the limit, and a title that flips without further release interrupts recovers. Extend agc_driver_eop_interrupts: a latched queue still delivers its own releases, flips keep reaching the video output while their event is suppressed, an event returns once the latch clears, and a release followed by two flips raises none. --- .../Execution/include/Driver/Driver.hpp | 2 +- .../src/Driver/Packets/PacketExecution.cpp | 10 +++++-- .../libs/prx/libSceAgcDriver/tests/Submit.cpp | 29 +++++++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp index 9c2fbd8573..2dbbcfce55 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp @@ -334,7 +334,7 @@ private: std::atomic flipBatchesUnsignaled{0}; std::mutex eopSelfSignalMutex; - std::unordered_set eopSelfSignalQueues; + std::unordered_map eopSelfSignalFlips; alignas(64) std::atomic packetsInFlight{0}; std::atomic packetsDone{0}; 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 a259b78eef..017b5f5bc8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -18,6 +18,8 @@ namespace AgcDriver::DriverDetail { namespace { +constexpr std::uint32_t eopSelfSignalFlipLimit = 4; + bool orderedFills() { static const bool ordered = std::getenv("APS5_PIPELINE_DRAIN_FILLS") == nullptr; return ordered; @@ -45,13 +47,17 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head void Driver::deliverGuestEopInterrupt(std::uint32_t queue) { if (!AgcDriverDeliverEopInterrupt(queue)) return; std::lock_guard lock(eopSelfSignalMutex); - eopSelfSignalQueues.insert(queue); + eopSelfSignalFlips[queue] = 0; } void Driver::deliverFlipEopInterrupt(std::uint32_t queue) { { std::lock_guard lock(eopSelfSignalMutex); - if (eopSelfSignalQueues.count(queue) != 0) return; + const auto latched = eopSelfSignalFlips.find(queue); + if (latched != eopSelfSignalFlips.end()) { + if (++latched->second <= eopSelfSignalFlipLimit) return; + eopSelfSignalFlips.erase(latched); + } } AgcDriverDeliverEopInterrupt(queue); } diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index d47d51e8ce..1ba3df5479 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -231,6 +231,35 @@ void testEndOfPipeInterrupts() { AgcDriverWaitIdle_nid_postfix(); check(output->ready.load() == 2, "flip after a release interrupt was not handed to its output"); check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip on a queue that already raised its own release interrupt raised a second event"); + words[2] = 1u << 24u; + check(sceAgcDriverSubmitDcb(&packet) == 0, "release before sustained flips submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a release interrupt did not deliver on a queue whose flip event is suppressed"); + std::uint32_t sustainedFlips = 0; + std::uint32_t flipsUntilEvent = 0; + while (sustainedFlips < 8 && flipsUntilEvent == 0) { + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "sustained flip submit failed"); + AgcDriverWaitIdle_nid_postfix(); + ++sustainedFlips; + check(output->ready.load() == static_cast(2 + sustainedFlips), "sustained flip was not handed to its output"); + if (owner->GetTriggeredEvents(events.data(), 2) != 0) flipsUntilEvent = sustainedFlips; + } + check(flipsUntilEvent != 0, "a flip never raised an event again on a queue whose own release interrupts stopped"); + check(flipsUntilEvent > 2, "a flip raised an event within two flips of the queue's own release interrupt"); + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after the latch cleared submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->ready.load() == static_cast(3 + sustainedFlips), "flip after the latch cleared was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a flip did not raise an event after the latch cleared"); + words[2] = 1u << 24u; + check(sceAgcDriverSubmitDcb(&packet) == 0, "release after a cleared latch submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(owner->GetTriggeredEvents(events.data(), 2) == 1, "a release interrupt did not deliver after the latch cleared"); + for (int index = 0; index < 2; ++index) { + check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip paired with a release interrupt submit failed"); + AgcDriverWaitIdle_nid_postfix(); + check(output->ready.load() == static_cast(4 + sustainedFlips + index), "flip paired with a release interrupt was not handed to its output"); + check(owner->GetTriggeredEvents(events.data(), 2) == 0, "a flip raised an event within two flips of the queue's own release interrupt"); + } AgcDriverUnregisterVideoOutput_nid_postfix(7, output); alignas(8) static volatile std::uint64_t label = 0; const auto labelAddress = reinterpret_cast(&label); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 67062ac81a..ca431abe34 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -163,6 +163,6 @@ - [Descriptor pool recycling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. -- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing after that. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Gran Turismo 7 (PPSA01317) and PPSA24156 pair more flips than release interrupts per frame, so the extra flip event reaches a consumer with no matching context and crashes. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. +- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving; a queue that flips four times without another interrupt from its own releases is unlatched and the fifth flip delivers again, so a title that raises a release interrupt once and then waits on the flip event recovers within five flips. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Gran Turismo 7 (PPSA01317) and PPSA24156 pair more flips than release interrupts per frame, so the extra flip event reaches a consumer with no matching context and crashes. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. From cb28abeb89b5260f3824932f265c3b1cc9801e63 Mon Sep 17 00:00:00 2001 From: Crimson <144462348+CrimsonMac@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:31:11 +0300 Subject: [PATCH 086/131] fix(shader): fault on misaligned 64-bit device-memory LDS atomics --- .../libSceAgcDriver/tests/LdsLockBudget.cpp | 7 +- .../tests/execution/LdsMixedWidthAtomics.cpp | 102 +++++++++++++++++- .../Optimization/src/ShaderInfoCollector.cpp | 6 ++ .../SpirvMemory/SpirvMemoryInstructions.cpp | 4 +- docs/dev/debt/shader.md | 2 +- 5 files changed, 115 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp index e69551e405..d409fc1d07 100644 --- a/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/LdsLockBudget.cpp @@ -60,13 +60,17 @@ std::uint64_t workgroupBytes(std::span words) { std::set compareExchangeWidths(std::span words) { std::map widths; std::set found; + std::uint32_t faultFunction = 0; + bool inFaultFunction = false; for (std::size_t offset = 5; offset < words.size();) { const auto count = words[offset] >> 16u; require(count != 0u && count <= words.size() - offset, "malformed SPIR-V instruction"); const auto instruction = words.subspan(offset, count); const auto opcode = static_cast(instruction[0] & 0xffffu); + if (opcode == spv::OpName && std::string_view(reinterpret_cast(instruction.data() + 2)) == "record_bda_fault") faultFunction = instruction[1]; + if (opcode == spv::OpFunction) inFaultFunction = instruction[2] == faultFunction; if (opcode == spv::OpTypeInt) widths.emplace(instruction[1], instruction[2]); - if (opcode == spv::OpAtomicCompareExchange) found.insert(widths.at(instruction[1])); + if (opcode == spv::OpAtomicCompareExchange && !inFaultFunction) found.insert(widths.at(instruction[1])); offset += count; } return found; @@ -110,6 +114,7 @@ void verify(std::uint32_t waveSize, std::uint32_t limit, bool int64Atomics) { const bool native = int64Atomics && limit < 68u; require(widths.contains(64u) == native, native ? "device-memory LDS did not use a 64-bit compare exchange" : "a 64-bit compare exchange appeared without device-memory LDS and shaderBufferInt64Atomics"); require(widths.contains(32u) != native, native ? "device-memory LDS with shaderBufferInt64Atomics kept the workgroup lock" : "the 64-bit LDS atomic lost its workgroup lock"); + require(result.bdaAbiVersion == (limit < 68u ? 1u : 0u), "the fault buffer for a misaligned 64-bit LDS atomic does not follow device-memory LDS at limit " + std::to_string(limit)); } } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp index f63631ee61..1f8032e93c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp @@ -14,12 +14,61 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#else +#include +#endif namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; +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; +}; + constexpr std::uint32_t Threads = 256; constexpr std::uint32_t Iterations = 16; constexpr std::uint32_t Repeats = 32; @@ -167,6 +216,32 @@ alignas(256) constexpr auto LoadedParityCode = Unrolled<1>( 0xbf810000, })); +alignas(256) constexpr auto MisalignedCode = Unrolled<1>( + std::to_array({ + 0x34100083, + 0x7e020280, + 0x7e0402c1, + 0x7e060280, + 0x7e080281, + 0x7e0c0280, + 0x7e0e0280, + 0xd9340000, 0x00000601, + 0xbf8cc07f, + 0xbf8a0000, + }), + std::to_array({ + 0xd9000004, 0x00000201, + 0xbf8cc07f, + }), + std::to_array({ + 0xbf8cc07f, + 0xbf8a0000, + 0xd9d80000, 0x0a000001, + 0xbf8cc07f, + 0xe0741000, 0x80010a08, + 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}; @@ -213,7 +288,7 @@ std::uint64_t WorkgroupBytes(std::span words) { return total; } -void Run(AgcDriver::VulkanDevice& device, std::span code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional expectedDeviceDwords = std::nullopt) { +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t ldsDwords) { 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)); @@ -228,10 +303,14 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, c {0, 0, 0, 128} }; request.useCache = false; - const auto result = ShaderRecompiler::Recompile(request); + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional expectedDeviceDwords = std::nullopt) { + const auto result = Compile(device, code, ldsDwords); if (expectedDeviceDwords) { const auto bytes = WorkgroupBytes(result.spirv.Words()); - const auto limit = request.target.maxWorkgroupSharedMemoryBytes; + const auto limit = device.Target().maxWorkgroupSharedMemoryBytes; Require(bytes <= limit, "lds lock budget: guest LDS and lock exceed the device limit before dispatch"); Require(result.workgroupMemoryDwords == *expectedDeviceDwords, "lds lock budget: unexpected device-backed LDS allocation before dispatch"); if (*expectedDeviceDwords == 0u) Require(bytes == limit, "lds lock budget: exact-fit case did not retain its lock"); @@ -246,6 +325,21 @@ void Run(AgcDriver::VulkanDevice& device, std::span code, c if (expectedDeviceDwords) std::printf("lds lock budget: %s GPU readback verified across 256 lanes\n", name); } +void Misaligned(AgcDriver::VulkanDevice& device, std::uint32_t ldsDwords) { + const auto result = Compile(device, MisalignedCode, ldsDwords); + StderrCapture capture; + try { + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(MisalignedCode.data())); + device.WaitIdle(); + } catch (...) { + static_cast(capture.Finish()); + throw; + } + const auto log = capture.Finish(); + Require(log.find("BDA access failed: address=0x4 instruction=0x2c bytes=8 ") != std::string::npos && log.find("reason=7") != std::string::npos, "lds lock budget: misaligned ds_add_u64 on device-backed LDS did not fault"); + std::puts("lds lock budget: misaligned ds_add_u64 faulted"); +} + void LockBudget(AgcDriver::VulkanDevice& device) { const auto target = device.Target(); const auto limit = target.maxWorkgroupSharedMemoryBytes; @@ -254,6 +348,8 @@ void LockBudget(AgcDriver::VulkanDevice& device) { Require(limit >= 32u && limit % 4u == 0u, "lds lock budget: unexpected device shared-memory limit"); const auto operations = static_cast(Threads) * Iterations; Run(device, UniformCode, "limit", operations, 1, limit / 4u, (limit / 4u + 2u) & ~1u); + const auto& capabilities = target.supportedCapabilities; + if (std::find(capabilities.begin(), capabilities.end(), static_cast(spv::CapabilityInt64Atomics)) != capabilities.end()) Misaligned(device, limit / 4u); Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, 0u); std::puts("lds lock budget tests passed"); } diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index fac95cb5d4..80871f8ea9 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -535,6 +535,12 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo return inst->Opcode() == IrOpcode::BitwiseXor32; }); }); + const bool ldsAtomic64 = std::ranges::any_of(program.Blocks(), [](const auto& block) { + return std::ranges::any_of(block->Instructions(), [](const IrValue* inst) { + return SharedAccessOf(inst->Opcode()) == SharedAccess::Atomic && inst->Type() == IrType::U64; + }); + }); + next.usesFaultBuffer = next.usesFaultBuffer || (next.sharedMemoryBytes != 0u && ldsAtomic64); switch (program.Resources().stage) { case IrShaderStage::Vertex: case IrShaderStage::Local: diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 417ce2c9de..85cb5aa621 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -1394,7 +1394,9 @@ std::uint32_t SharedAtomic64(SpirvValueEmitContext& ctx, const IrValue& inst, TR const auto high = EmitMemoryElementIndex(state, resource, Binary(state, spv::OpIAdd, TypeU32(state), rawIndex, ConstantU32(state, 1u))); return EmitValueOrDefaultIfCondition(state, EmitMemoryElementInBounds(state, resource, high), TypeU64(state), ConstantU64(state, 0u), [&]() { if (state.requirements.ldsBufferInt64Atomics) { - const auto aligned = Binary(state, spv::OpIEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), low, ConstantU32(state, 1u)), ConstantU32(state, 0u)); + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), low, ConstantU32(state, 1u)), ConstantU32(state, 0u)); + EmitIfCondition(state, unaligned, [&] { RecordBdaFaultWords(state, Binary(state, spv::OpShiftLeftLogical, TypeU32(state), rawIndex, ConstantU32(state, 2u)), ConstantU32(state, 0u), ConstantU32(state, 8u), ConstantU32(state, inst.Flags().pc), BdaAbi::FaultReason::Unaligned); }); + const auto aligned = Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned); return EmitValueOrDefaultIfCondition(state, aligned, TypeU64(state), ConstantU64(state, 0u), [&]() { return LdsBufferAtomic64(state, low, [&](std::uint32_t old) { return replacement(state, old); }); }); } const auto update = [&]() { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 3f58b62d0a..85a3ad935c 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -129,7 +129,7 @@ - [Negative color-texture comparison wrap indices](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (libSceAgcDriver) use explicit `OpSRem` normalization to implement modulo; the negative-offset point-wrap regression fails with direct `OpSMod` and passes with normalization on Mesa 22.3.6 llvmpipe (LLVM 15.0.6), but negative-index wrap has not been measured on PS5 hardware. - [Converted unorm images](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 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, 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. +- [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. - [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. From 127ee82f0277d4c4957cbe985f504ceca460a731 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sun, 18 Oct 2026 12:03:18 +0200 Subject: [PATCH 087/131] feat(libc): dump the faulting thread's stack words in Windows crash reports APS5_CRASH_STACK_BYTES= prints n bytes of stack words from rsp after the stack return address candidates, then eight words at every distinct stack word that points at readable, non-executable memory outside the thread's stack. Off when the variable is unset, empty or 0. --- core/libs/prx/libc/CMakeLists.txt | 9 ++++ .../src/specifics/windows/CrashReport.cpp | 54 ++++++++++++++++++- core/libs/tests/WindowsBacktraceReport.cpp | 18 +++++++ docs/user/USAGE.md | 2 + 4 files changed, 82 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 7344519b44..deee94494f 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -837,5 +837,14 @@ if(MINGW) ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$" PASS_REGULAR_EXPRESSION "kept failure report ran[^~]*aborting[^~]*SIGABRT. on thread[^~]*all threads:" FAIL_REGULAR_EXPRESSION "kept failure report ran[^~]*kept failure report ran") + add_test(NAME windows_crash_stack_words COMMAND windows_backtrace_report_tests fault) + set_tests_properties(windows_crash_stack_words PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=set:2048" + PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005[^~]*stack words from rsp[^~]*data at stack words that point outside the stack[^~]*c0ffee0000000000 c0ffee0000000001 c0ffee0000000002 c0ffee0000000003 c0ffee0000000004 c0ffee0000000005 c0ffee0000000006 c0ffee0000000007") + add_test(NAME windows_crash_without_stack_words COMMAND windows_backtrace_report_tests fault) + set_tests_properties(windows_crash_without_stack_words PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=unset:" + PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005" + FAIL_REGULAR_EXPRESSION "stack words from rsp;data at stack words") endif() endif() diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index 47cfaed9fc..e7c1694e1a 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -256,6 +256,57 @@ bool ReadWord(std::uint64_t address, std::uint64_t* value) { return ReadCode(address, reinterpret_cast(value), sizeof(*value)) == sizeof(*value); } +std::uint64_t EnvironmentNumber(const char* name) { + char text[32]; + const DWORD length = GetEnvironmentVariableA(name, text, sizeof(text)); + if (length == 0 || length >= sizeof(text)) return 0; + return std::strtoull(text, nullptr, 0); +} + +void ReportStackWords(std::uint64_t rsp) { + const auto bytes = std::min(EnvironmentNumber("APS5_CRASH_STACK_BYTES"), std::uint64_t{1} << 20); + if (bytes == 0) return; + const auto* tib = reinterpret_cast(NtCurrentTeb()); + const auto stackLow = reinterpret_cast(tib->StackLimit); + const auto stackHigh = reinterpret_cast(tib->StackBase); + const auto first = rsp & ~std::uint64_t{7}; + Report(" stack words from rsp 0x%016llx (APS5_CRASH_STACK_BYTES):\n", static_cast(rsp)); + std::uint64_t end = first; + std::uint64_t readablePage = 0; + for (std::uint64_t at = first; at < first + bytes; at += 32) { + std::uint64_t words[4] = {}; + int count = 0; + for (; count < 4; ++count) { + const auto slot = at + static_cast(count) * 8; + const auto page = slot & ~std::uint64_t{0xfff}; + if (page != readablePage) { + if (!IsReadable(slot)) break; + readablePage = page; + } + words[count] = *reinterpret_cast(slot); + } + if (count == 0) break; + char text[192]; + int used = std::snprintf(text, sizeof(text), " +0x%04llx:", static_cast(at - first)); + for (int k = 0; k < count && used > 0 && used < static_cast(sizeof(text)); ++k) used += std::snprintf(text + used, sizeof(text) - static_cast(used), " %016llx", static_cast(words[k])); + Report("%s\n", text); + end = at + static_cast(count) * 8; + if (count < 4) break; + } + std::uint64_t seen[256]; + std::size_t seenCount = 0; + Report(" data at stack words that point outside the stack (8 words each):\n"); + for (std::uint64_t slot = first; slot + 8 <= end && seenCount < std::size(seen); slot += 8) { + const auto value = *reinterpret_cast(slot); + if (value < 0x10000 || (value & 7) != 0 || value > 0x7fffffffffffull || (value >= stackLow && value < stackHigh)) continue; + if (std::find(seen, seen + seenCount, value) != seen + seenCount) continue; + if (IsExecutable(value) || !IsReadable(value) || !IsReadable(value + 0x38)) continue; + seen[seenCount++] = value; + const auto* words = reinterpret_cast(value); + Report(" [+0x%04llx] 0x%016llx: %016llx %016llx %016llx %016llx %016llx %016llx %016llx %016llx\n", static_cast(slot - first), static_cast(value), words[0], words[1], words[2], words[3], words[4], words[5], words[6], words[7]); + } +} + void ReportUndeliverableException(const EXCEPTION_POINTERS* info, std::uint64_t frame) { const auto* record = info->ExceptionRecord; char type[256] = "of unknown type"; @@ -377,7 +428,8 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { frame = next; } ReportStackCandidates(context->Rsp, 16, 0x2000); - if (context->Rip == 0 || !IsExecutable(context->Rip)) { + ReportStackWords(context->Rsp); + if (context->Rip == 0|| !IsExecutable(context->Rip)) { // A jump into nothing usually follows a return through a clobbered frame; the frames that just // returned are still below rsp. Report(" recently popped return address candidates:\n"); diff --git a/core/libs/tests/WindowsBacktraceReport.cpp b/core/libs/tests/WindowsBacktraceReport.cpp index 32871b4f63..d16340e2b3 100644 --- a/core/libs/tests/WindowsBacktraceReport.cpp +++ b/core/libs/tests/WindowsBacktraceReport.cpp @@ -1,6 +1,8 @@ +#include #include #include #include +#include #include "../prx/libc/include/general/VabiMacros.hpp" #include "../prx/libc/include/FailureReport.hpp" @@ -16,10 +18,26 @@ void DroppedReport() noexcept { std::fputs("dropped failure report ran\n", stderr); } +LONG WINAPI EndAfterReport(EXCEPTION_POINTERS* info) { + if (info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) TerminateProcess(GetCurrentProcess(), 3); + return EXCEPTION_CONTINUE_SEARCH; +} + +[[gnu::noinline]] void FaultHoldingObject() { + AddVectoredExceptionHandler(0, EndAfterReport); + auto* object = static_cast(VirtualAlloc(nullptr, 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)); + if (object == nullptr) std::exit(1); + for (std::uint64_t i = 0; i < 8; ++i) object[i] = 0xc0ffee0000000000ull + i; + std::uint64_t* volatile held = object; + volatile int* volatile target = nullptr; + *target = static_cast(reinterpret_cast(held)); +} + } int main(int argc, char** argv) { if (__error_nid_postfix() == nullptr) return 1; + if (argc == 2 && std::strcmp(argv[1], "fault") == 0) FaultHoldingObject(); if (argc == 2 && std::strcmp(argv[1], "report") == 0) { LibcRegisterFailureReport_nid_no_patch(DroppedReport); LibcRegisterFailureReport_nid_no_patch(KeptReport); diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 27b1862066..de73e72082 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -242,6 +242,8 @@ When a register points at readable memory that starts with at least four printab Set `APS5_CRASH_INDIRECT_MEMORY=1` before launching the executable to include up to 64 bytes from each pointer found in the register memory dump. The report labels each block with its register, pointer offset and target address. Inaccessible, incomplete and executable targets are skipped. This additional dump is disabled when the variable is unset or empty. +Set `APS5_CRASH_STACK_BYTES=` to print, after the stack return address candidates, the faulting thread's stack words from `rsp` for `n` bytes (at most 1048576, decimal or `0x` hexadecimal), four words per line with their offset from `rsp`. Below them the report prints eight words at every distinct stack word that points at readable, non-executable memory outside the thread's stack, labelled with the word's offset and the address it holds, up to 256 addresses. This shows the objects the caller frames kept, such as the `this` of a destructor or the buffer of a container. The dump stops at the first unreadable stack page, and nothing is printed when the variable is unset, empty or 0. + ### GPU submission rings and stall dumps Each GPU queue keeps its last 16 command buffer submissions: when each was submitted, its serial, size and guest address, and a copy of its first 512 dwords after indirect buffers are inlined. `APS5_PM4_RING=` keeps the last `n` instead, from 0 to 4096; 0 keeps none, and any other value stops the title at its first submission. The rings are printed to stderr, one line per submission and one per decoded packet, when the Windows crash report runs: an unhandled exception, `abort()`, an invalid CRT parameter or the timed dump of `APS5_HANG_DUMP_SECS`. From 863e3f5815807f1bfacd8a418ab650f28c7afd56 Mon Sep 17 00:00:00 2001 From: Lberonio <103309975+Lberonio@users.noreply.github.com> Date: Sun, 18 Oct 2026 13:52:37 +0300 Subject: [PATCH 088/131] docs(agc): record why the flip event latch cannot be bounded in time --- 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 ca431abe34..0cee133305 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -163,6 +163,6 @@ - [Descriptor pool recycling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. -- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving; a queue that flips four times without another interrupt from its own releases is unlatched and the fifth flip delivers again, so a title that raises a release interrupt once and then waits on the flip event recovers within five flips. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Gran Turismo 7 (PPSA01317) and PPSA24156 pair more flips than release interrupts per frame, so the extra flip event reaches a consumer with no matching context and crashes. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. +- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving; a queue that flips four times without another interrupt from its own releases is unlatched and the fifth flip delivers again, so a title that raises a release interrupt once and then waits on the flip event recovers within five flips. The bound is by further flips and cannot be by time: the release interrupt that follows a flip can arrive seconds later than the flip across a title's loading stalls, and an unexpected flip event crashes a title that pairs every flip with a release, so no window short enough to unblock a title waiting on its first suppressed flip keeps such a title alive. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Gran Turismo 7 (PPSA01317) and PPSA24156 pair more flips than release interrupts per frame, so the extra flip event reaches a consumer with no matching context and crashes. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue, which under this rule stays blocked until its queue flips again. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. From 9de7cfd9f444f8f96ecc3161ec591593be70ae25 Mon Sep 17 00:00:00 2001 From: Lberonio <103309975+Lberonio@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:26:35 +0300 Subject: [PATCH 089/131] docs(agc): clarify flip latch clearing condition --- 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 0cee133305..74262e4617 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -163,6 +163,6 @@ - [Descriptor pool recycling](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) avoids individually freed sets: [RADV 26.2.2](https://gitlab.freedesktop.org/mesa/mesa/-/blob/mesa-26.2.2/src/amd/vulkan/radv_descriptor_pool.c) initializes its free-set heap with 32 bytes beyond the backing allocation. An allocation-only RX 6600 test reproduces acceptance of a 4,128-byte set in a 4,096-byte pool; the path without individual frees rejects it. Pools reset after their last set is released. A cached set can retain unused slots in an exhausted pool until eviction; peak gameplay pool memory remains unmeasured. - [16-bit depth with stencil](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) is stored as `D24_UNORM_S8_UINT`, or `D32_SFLOAT_S8_UINT`, on a device without `D16_UNORM_S8_UINT` as an attachment. Depth comparisons, clears and the depth image see the wider format's precision, so results at the 16-bit quantization boundary can differ from the console; the constant depth bias is scaled by 256 for D24, and a non-zero constant on the D32 float fallback throws. Not measured on console hardware. - [Depth surfaces created over storage writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a depth surface created without HTILE copies its depth plane from the storage image at the plane's address, a pending one first, when that image has the plane's extent, one level, a 2D view or a 2D array view (its layer 0 is copied) and a 16-bit format for a 16-bit plane or a 32-bit format for a 32-bit plane. A multisampled surface, a surface the device stores in another format, a surface with HTILE and a surface without such an image start from `DB_DEPTH_CLEAR`, and the stencil plane from `DB_STENCIL_CLEAR`; CPU writes to the plane's memory are not read. Not measured on the console, where the plane is that memory. -- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving; a queue that flips four times without another interrupt from its own releases is unlatched and the fifth flip delivers again, so a title that raises a release interrupt once and then waits on the flip event recovers within five flips. The bound is by further flips and cannot be by time: the release interrupt that follows a flip can arrive seconds later than the flip across a title's loading stalls, and an unexpected flip event crashes a title that pairs every flip with a release, so no window short enough to unblock a title waiting on its first suppressed flip keeps such a title alive. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Gran Turismo 7 (PPSA01317) and PPSA24156 pair more flips than release interrupts per frame, so the extra flip event reaches a consumer with no matching context and crashes. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue, which under this rule stays blocked until its queue flips again. +- [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does, but only on a queue that has never delivered an interrupt from such a release: the queue is latched the first time a release interrupt reaches the event queue, and flips on that queue deliver nothing while its own release interrupts keep arriving. The latch is cleared when the queue has flipped five times without another interrupt from its own releases: the fifth flip delivers the event and unlatched the queue, so a title that waits on the flip event after latching recovers only if it submits further flips. The bound cannot be by time: the next release interrupt can arrive seconds later than a suppressed flip across loading stalls, and delivering an unexpected flip event on a queue that pairs flips with releases can crash; no fixed window short enough to unblock a waiting title keeps such a title alive. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; nor is how the console behaves for a title that waits on the flip event and also raises release interrupts on the same queue. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. From 49ad10b771213a69ed4a816fd22d0f07a8af0248 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:15:26 -0400 Subject: [PATCH 090/131] feat(agc): sample 8 and 8_8 sRGB textures through an RGBA8 sRGB copy A device that cannot sample R8_SRGB or R8G8_SRGB had its sRGB textures decoded in the shader, which cannot serve filtered samples: the decode must come before the filter. The texture is now an RGBA8 UNORM image viewed as RGBA8 sRGB, filled by a one-to-one blit of the R8 or RG8 texels (a snapshot's upload, or a storage image's texels refreshed when its Version changes), so the sampler decodes and then filters, and every sample, gather and load of these images is an ordinary sRGB one. The recompiler is given the decode mask only for formats the device cannot blit, or when APS5_SRGB_SHADER_DECODE=1 forces the shader decode. Tests run on lavapipe only. --- .../Execution/src/VulkanDevice.cpp | 5 +- .../Graphics/include/Context.hpp | 1 + .../Graphics/include/Texture.hpp | 6 + .../Graphics/include/TextureFormat.hpp | 2 + .../Graphics/src/ShaderResources.cpp | 14 +- .../libSceAgcDriver/Graphics/src/Texture.cpp | 167 ++++++++- .../Graphics/src/TextureFormat.cpp | 20 ++ .../tests/execution/ImageSrgbSampleCopy.cpp | 319 ++++++++++++++++++ 8 files changed, 526 insertions(+), 8 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bdbb94ebe8..2fc64d1995 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -324,6 +324,7 @@ struct VulkanDevice::State { bool pipelineExecutableInfo = false; bool maintenance8 = false; std::uint32_t srgbDecodeFormats = 0; + std::uint32_t srgbCopyFormats = 0; bool storageImageWithoutFormat = false; VkSampleCountFlags sampleLocationSampleCounts = 0; VkSampleCountFlags sampleLocationGridSampleCounts = 0; @@ -1111,6 +1112,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) : state->properties = properties.properties; state->physical = selected; state->srgbDecodeFormats = Graphics::SrgbDecodeFormats(state->InstanceFunction("vkGetPhysicalDeviceFormatProperties"), selected); + state->srgbCopyFormats = Graphics::SrgbCopyFormats(state->InstanceFunction("vkGetPhysicalDeviceFormatProperties"), selected, state->srgbDecodeFormats); if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0) { state->capabilities.push_back(spv::CapabilityGroupNonUniform); if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformBallot); @@ -3506,7 +3508,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const { target.fixedPushSlots = state->graphicsPipelineLibrary; target.registrationModules = state->graphicsPipelineLibrary; target.robustBufferAccess2 = state->robustBufferAccess2; - target.srgbDecodeFormats = state->srgbDecodeFormats; + target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats; target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0; if (state->meshShader) { const auto& mesh = state->meshLimits; @@ -3647,6 +3649,7 @@ Graphics::Context VulkanDevice::buildContext() const { context.imageViewMinLod = state->imageViewMinLod; context.pipelineExecutableInfo = state->pipelineExecutableInfo; context.srgbDecodeFormats = state->srgbDecodeFormats; + context.srgbCopyFormats = state->srgbCopyFormats; context.sampleLocationSampleCounts = state->sampleLocationSampleCounts; context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts; context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction("vkGetPhysicalDeviceMemoryProperties2") : nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 8eaada9e84..d5f3cd7fee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -229,6 +229,7 @@ struct Context { bool graphicsPipelineLibrary = false; bool pipelineLinkTimeOptimization = true; std::uint32_t srgbDecodeFormats = 0; + std::uint32_t srgbCopyFormats = 0; bool provokingVertexLast = false; bool provokingVertexModePerPipeline = false; bool singlePassStorage = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 344d5ee592..68869367e8 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -66,6 +66,9 @@ public: Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr owner = {}); struct DepthCopyTag {}; Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag); + struct SrgbExpandTag {}; + Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, SrgbExpandTag); + static bool CanSrgbExpandFrom(const Context& context, const StorageTexture& source, const GuestTextureResource& descriptor); static VkFormat DepthCopyFormat(VkFormat colorFormat); static bool CanDepthCopyFrom(const StorageTexture& source, const GuestTextureResource& descriptor); struct BlockCopyTag {}; @@ -117,6 +120,7 @@ public: private: void release() noexcept; bool refreshBlockCopy(); + bool refreshSrgbExpand(); void createFirstLayerView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareBaseLevelView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); void prepareSrgbView(const GuestTextureResource& descriptor, VkImageViewCreateInfo viewInfo); @@ -147,6 +151,8 @@ private: std::unique_ptr upload; PerUseCopy perUseCopy = PerUseCopy::None; bool copiedOnce = false; + bool srgbExpand = false; + GuestTextureResource sourceDescriptor{}; std::uint64_t copiedVersion = 0; VkFormat depthFormat = VK_FORMAT_UNDEFINED; VkFormat blockFormat = VK_FORMAT_UNDEFINED; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index ff9eaf11df..9271395ec1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -16,6 +16,8 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat); VkComponentSwizzle TextureComponentChannel(std::uint32_t guestFormat, VkComponentSwizzle component); std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical); VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat); +std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats); +bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat); bool IsSrgbTextureFormat(std::uint32_t guestFormat); std::optional FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes); std::optional FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 36d2c9aa87..a08d68a71b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -636,10 +636,11 @@ std::shared_ptr cachedTexture(const Context& context, std::span= DepthCopyMinBytes() && Texture::DepthCopyFormat(ResolveTextureFormat(resource.format)) != VK_FORMAT_UNDEFINED && resource.dimension != TextureDimension::k3D; + const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, resource.format); auto source = StorageTexture::FindPending(address, guestBytes); - const bool blockCopy = source != nullptr && !depthCompare && Texture::CanBlockCopyFrom(*source, resource); + const bool blockCopy = source != nullptr && !depthCompare && !srgbExpand && Texture::CanBlockCopyFrom(*source, resource); const bool repackCopy = source != nullptr && !depthCompare && Texture::CanRepackCopyFrom(*source, resource); - if (source != nullptr && !blockCopy && !repackCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); + if (source != nullptr && !blockCopy && !repackCopy && (depthCompare ? !depthCopy || !Texture::CanDepthCopyFrom(*source, resource) : srgbExpand ? !Texture::CanSrgbExpandFrom(context, *source, resource) : !Texture::CanCopyFrom(*source, resource))) source.reset(); if (source != nullptr && MetadataMoved(*source, resource)) source.reset(); std::optional keys; bool clearThroughKeys = false; @@ -671,6 +672,7 @@ std::shared_ptr cachedTexture(const Context& context, std::span cachedTexture(const Context& context, std::spanRefreshDepthCopy(); return texture; } + if (source != nullptr && srgbExpand) { + auto texture = std::make_shared(context, source, resource, components, Texture::SrgbExpandTag{}); + texture->RefreshDepthCopy(); + return texture; + } if (source != nullptr) return std::make_shared(context, source, resource, components); std::vector snapshot(bytes); ReadTextureSurface(resource, *keys, snapshot); @@ -813,6 +820,9 @@ std::shared_ptr cachedTexture(const Context& context, std::span(context, source, resource, components, Texture::RepackCopyTag{}); entry.texture->RefreshDepthCopy(); + } else if (srgbExpand) { + entry.texture = std::make_shared(context, source, resource, components, Texture::SrgbExpandTag{}); + entry.texture->RefreshDepthCopy(); } else { entry.texture = std::make_shared(context, source, resource, components); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 270d9ca0f8..b2870d8232 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -232,6 +232,53 @@ void ChainMinLod(const Context& context, const GuestTextureResource& descriptor, viewInfo.pNext = &minLod; } +struct ExpandImage { + VkImage image = VK_NULL_HANDLE; + std::shared_ptr owned; + VkDeviceSize bytes = 0; +}; + +ExpandImage CreateExpandImage(const Context& context, const GuestTextureResource& descriptor, std::uint32_t depth, std::uint32_t layers) { + ExpandImage result; + VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + imageInfo.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | (descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u); + imageInfo.imageType = ImageTypeFor(descriptor.dimension); + imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM; + imageInfo.extent = {descriptor.width, descriptor.height, depth}; + imageInfo.mipLevels = descriptor.mipCount; + imageInfo.arrayLayers = layers; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + 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; + Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &result.image), "vkCreateImage sRGB copy"); + result.owned = std::make_shared(context, result.image, VK_NULL_HANDLE); + VkMemoryRequirements requirements{}; + context.Function("vkGetImageMemoryRequirements")(context.device, result.image, &requirements); + VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; + allocation.allocationSize = requirements.size; + allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture"); + Check(context.Function("vkAllocateMemory")(context.device, &allocation, nullptr, &result.owned->memory), "vkAllocateMemory sRGB copy"); + Check(context.Function("vkBindImageMemory")(context.device, result.image, result.owned->memory, 0), "vkBindImageMemory sRGB copy"); + result.bytes = requirements.size; + return result; +} + +void RecordExpandBlit(const Context& context, VkCommandBuffer commands, VkImage source, VkImageLayout sourceLayout, VkImage target, const GuestTextureResource& descriptor, std::uint32_t depth, std::uint32_t layers) { + std::vector regions; + for (std::uint32_t level = 0; level < descriptor.mipCount; ++level) { + const VkOffset3D end{static_cast(std::max(descriptor.width >> level, 1u)), static_cast(std::max(descriptor.height >> level, 1u)), static_cast(descriptor.dimension == TextureDimension::k3D ? std::max(depth >> level, 1u) : 1u)}; + VkImageBlit region{}; + region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, layers}; + region.srcOffsets[1] = end; + region.dstSubresource = region.srcSubresource; + region.dstOffsets[1] = end; + regions.push_back(region); + } + context.Function("vkCmdBlitImage")(commands, source, sourceLayout, target, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data(), VK_FILTER_NEAREST); +} + std::atomic& SampledMemoryCounter() { static std::atomic bytes{0}; return bytes; @@ -249,6 +296,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex PhaseTimer timer; try { const auto colorFormat = SampledTextureFormat(context, descriptor.format); + const bool srgbExpand = !depthCompare && SrgbExpandsToRgba8(context, descriptor.format); Require(!depthCompare || colorFormat == VK_FORMAT_R32_SFLOAT || colorFormat == VK_FORMAT_R16_UNORM, "comparison sampling requires an R32 float or R16 unorm depth texture"); Require(!depthCompare || descriptor.dimension != TextureDimension::k3D, "comparison sampling does not support 3D depth textures"); const auto vkFormat = depthCompare ? (colorFormat == VK_FORMAT_R32_SFLOAT ? VK_FORMAT_D32_SFLOAT : VK_FORMAT_D16_UNORM) : colorFormat; @@ -281,7 +329,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex imageInfo.arrayLayers = geometry.imageLayers; imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; - imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | (srgbExpand ? VK_IMAGE_USAGE_TRANSFER_SRC_BIT : 0u); imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &imageInfo, nullptr, &image), "vkCreateImage"); @@ -296,6 +344,16 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex owned->memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory texture"); Check(context.Function("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory"); + const VkImage uploadImage = image; + std::shared_ptr uploadOwned; + if (srgbExpand) { + auto expanded = CreateExpandImage(context, descriptor, geometry.imageDepth, geometry.imageLayers); + uploadOwned = std::move(owned); + owned = std::move(expanded.owned); + image = expanded.image; + allocationBytes = expanded.bytes; + } + { // Debug aid: APS5_DUMP_TEXTURE= saves the detiled first mip // of those sampled textures on their first 8 uploads as texture_
_.raw (u32 @@ -334,6 +392,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex recorder->Keep(tiled, static_cast(guestBytes)); recorder->Keep(linear, static_cast(linearBytes)); recorder->Keep(owned); + if (uploadOwned != nullptr) recorder->Keep(uploadOwned); } else { batch = std::make_unique(context); commands = batch->Handle(); @@ -379,7 +438,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - toTransferDst.image = image; + toTransferDst.image = uploadImage; toTransferDst.subresourceRange = {aspect, 0, descriptor.mipCount, 0, geometry.imageLayers}; context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &linearReadBarrier, 1, &toTransferDst); @@ -400,7 +459,19 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex regions.push_back(region); } } - context.Function("vkCmdCopyBufferToImage")(commands, linear->Handle(), image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + context.Function("vkCmdCopyBufferToImage")(commands, linear->Handle(), uploadImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast(regions.size()), regions.data()); + if (srgbExpand) { + std::array toBlit{}; + toBlit[0] = toTransferDst; + toBlit[0].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toBlit[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + toBlit[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toBlit[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + toBlit[1] = toTransferDst; + toBlit[1].image = image; + context.Function("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 2, toBlit.data()); + RecordExpandBlit(context, commands, uploadImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, image, descriptor, geometry.imageDepth, geometry.imageLayers); + } std::unique_ptr dump; if (dumpWanted) { dump = std::make_unique(context, static_cast(mips[0].linearSize), VK_BUFFER_USAGE_TRANSFER_DST_BIT); @@ -451,8 +522,8 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image; viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); - viewInfo.format = vkFormat; - viewFormat = vkFormat; + viewInfo.format = srgbExpand ? VK_FORMAT_R8G8B8A8_SRGB : vkFormat; + viewFormat = viewInfo.format; viewInfo.components = depthCompare ? VkComponentMapping{} : components; viewInfo.subresourceRange = {aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; @@ -687,10 +758,96 @@ Texture::Texture(const Context& context, const std::shared_ptr& } } +bool Texture::CanSrgbExpandFrom(const Context& context, const StorageTexture& source, const GuestTextureResource& descriptor) { + if (!SrgbExpandsToRgba8(context, descriptor.format) || !CanCopyFrom(source, descriptor) || descriptor.dimension == TextureDimension::k3D) return false; + if (!StorageFormatAvailable(context, source.Descriptor().format) || StorageFormatForGuest(context, source.Descriptor().format) != SampledTextureFormat(context, descriptor.format)) return false; + const auto geometry = DescribeSurface(descriptor); + return geometry.imageDepth == 1 && source.ImageDepth() == 1 && source.ImageLayers() >= geometry.imageLayers && source.Descriptor().mipCount >= descriptor.mipCount; +} + +Texture::Texture(const Context& context, const std::shared_ptr& source, const GuestTextureResource& descriptor, VkComponentMapping components, SrgbExpandTag) : context(context), storageSource(source), perUseCopy(PerUseCopy::RefreshedOnReuse), srgbExpand(true) { + try { + Require(source != nullptr && CanSrgbExpandFrom(context, *source, descriptor), "storage image cannot serve an sRGB copy of the sampled texture"); + const auto geometry = DescribeSurface(descriptor); + copyWidth = descriptor.width; + copyHeight = descriptor.height; + copyLevels = descriptor.mipCount; + copyLayers = geometry.imageLayers; + APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u layers=%u sRGB copy of storage image (guest format %u)", static_cast(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, descriptor.format); + auto expanded = CreateExpandImage(context, descriptor, 1u, copyLayers); + image = expanded.image; + owned = std::move(expanded.owned); + allocationBytes = expanded.bytes; + + const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u; + const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray; + if (descriptor.dimension == TextureDimension::kCube) { + Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices"); + } + VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + viewInfo.image = image; + viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount); + viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB; + viewFormat = viewInfo.format; + viewInfo.components = components; + viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount}; + VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT}; + ChainMinLod(context, descriptor, viewInfo, minLod); + Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView sRGB copy"); + viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount}; + createFirstLayerView(descriptor, viewInfo); + prepareBaseLevelView(descriptor, viewInfo); + sourceDescriptor = descriptor; + countedBytes = allocationBytes; + SampledMemoryCounter().fetch_add(countedBytes, std::memory_order_relaxed); + } catch (...) { + release(); + throw; + } +} + +bool Texture::refreshSrgbExpand() { + auto* recorder = Recorder::Active(); + std::unique_ptr batch; + VkCommandBuffer commands = VK_NULL_HANDLE; + if (recorder != nullptr) { + commands = recorder->Commands(); + recorder->Keep(owned); + recorder->Keep(storageSource); + } else { + batch = std::make_unique(context); + commands = batch->Handle(); + } + const auto barrier = context.Function("vkCmdPipelineBarrier"); + const VkMemoryBarrier produced{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT}; + VkImageMemoryBarrier toTransferDst{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + toTransferDst.srcAccessMask = VK_ACCESS_SHADER_READ_BIT; + toTransferDst.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toTransferDst.oldLayout = copiedOnce ? VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : VK_IMAGE_LAYOUT_UNDEFINED; + toTransferDst.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toTransferDst.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + toTransferDst.image = image; + toTransferDst.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, copyLevels, 0, copyLayers}; + barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &produced, 0, nullptr, 1, &toTransferDst); + RecordExpandBlit(context, commands, storageSource->Image(), VK_IMAGE_LAYOUT_GENERAL, image, sourceDescriptor, 1u, copyLayers); + VkImageMemoryBarrier toShaderRead = toTransferDst; + toShaderRead.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + toShaderRead.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + toShaderRead.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + toShaderRead.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toShaderRead); + if (batch) batch->SubmitAndWait(); + copiedOnce = true; + copiedVersion = storageSource->Version(); + return true; +} + bool Texture::RefreshDepthCopy() { if (perUseCopy != PerUseCopy::RefreshedOnReuse || storageSource == nullptr) return false; if (copiedOnce && storageSource->Version() == copiedVersion) return false; if (blockFormat != VK_FORMAT_UNDEFINED) return refreshBlockCopy(); + if (srgbExpand) return refreshSrgbExpand(); std::uint64_t linearBytes = 0; for (std::uint32_t level = 0; level < copyLevels; ++level) linearBytes += static_cast(std::max(copyWidth >> level, 1u)) * std::max(copyHeight >> level, 1u) * copyElementBytes * copyLayers; auto linear = std::make_shared(context, static_cast(linearBytes), VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT | (repackCopy ? VK_BUFFER_USAGE_STORAGE_BUFFER_BIT : 0u)); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index 59dd54b906..e72bae96fc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -137,6 +137,26 @@ VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat) return ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))); } +std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats) { + const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + if (forced != nullptr && std::strcmp(forced, "1") == 0) return 0u; + VkFormatProperties target{}; + formatProperties(physical, VK_FORMAT_R8G8B8A8_UNORM, &target); + if ((target.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT) == 0) return 0u; + std::uint32_t formats = 0; + for (const auto format : {ShaderRecompiler::IrBufferFormat::Format8Srgb, ShaderRecompiler::IrBufferFormat::Format8_8Srgb}) { + if ((decodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) continue; + VkFormatProperties source{}; + formatProperties(physical, ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))), &source); + if ((source.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT) != 0) formats |= ShaderRecompiler::SrgbDecodeBit(format); + } + return formats; +} + +bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat) { + return (context.srgbCopyFormats & ShaderRecompiler::SrgbDecodeBit(static_cast(guestFormat))) != 0u; +} + bool IsSrgbTextureFormat(std::uint32_t guestFormat) { switch (findFormatEntry(guestFormat).vkFormat) { case VK_FORMAT_R8_SRGB: diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp new file mode 100644 index 0000000000..ed83b35bd8 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp @@ -0,0 +1,319 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.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 "Recompiler.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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Size = 4; +constexpr std::uint32_t Type2D = 9; +constexpr std::uint32_t Srgb8 = 128; +constexpr std::uint32_t Srgb8_8 = 129; +constexpr std::uint32_t SwizzleX = 0xfacu; +constexpr std::uint32_t SwizzleYAsX = 0xfadu; +constexpr std::uint32_t FilterPoint = 0; +constexpr std::uint32_t FilterBilinear = 1; +constexpr std::array Codes{0, 255, 128, 64}; +constexpr std::size_t BlockBytes = 2u << 20u; +constexpr std::uint32_t StoreSize = 1024; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array SampleCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) std::array StoreCode{ + 0x7e280d00, 0x102828ff, 0x3d000000, 0x062a28ff, 0x41000000, 0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x0001141e, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array GatherCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf11c0108, 0x00820401, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + + +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, "image sRGB sample copy: cannot allocate the guest block"); + Require(AgcDriver::GuestMemory::Watched(reinterpret_cast(block), BlockBytes), "image sRGB sample copy: the guest block is not write-watched"); + std::memset(block, 0, BlockBytes); + GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true); + AgcDriver::GuestMemory::CollectWritesUncached(reinterpret_cast(block), BlockBytes); + } + + ~GuestBlock() { + GuestAllocations::Mutation().Remove(block); +#ifdef _WIN32 + GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes); + GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes); +#else + munmap(block, BlockBytes); + GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes); +#endif + } + + 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(std::uint32_t format, std::uint32_t swizzle, const void* data = Texels.data(), std::uint32_t extent = Size) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (format << 20u) | (((extent - 1u) & 3u) << 30u), + ((extent - 1u) >> 2u) | ((extent - 1u) << 14u), + swizzle | (Type2D << 28u), + 0u, + 0u, + 0u, + 0u, + }; +} + +std::array SamplerDescriptor(std::uint32_t filter) { + return {0x92u, 0x00fff000u, (filter << 20u) | (filter << 22u) | (1u << 24u), 0u}; +} + +double Decode(std::uint32_t code) { + const double encoded = code / 255.0; + return encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4); +} + +std::uint32_t Code(std::uint32_t format, bool second, std::uint32_t x) { + const std::uint32_t code = Codes.at(std::min(x, Size - 1u)); + return second && format == Srgb8_8 ? 255u - code : code; +} + +void FillTexels(std::uint32_t format) { + const std::uint32_t bytes = format == Srgb8 ? 1u : 2u; + const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format, Size, Size, 1); + Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sRGB sample copy: the texture does not fit the texel storage"); + Texels.fill(0); + for (std::uint32_t y = 0; y < Size; ++y) { + for (std::uint32_t x = 0; x < Size; ++x) { + auto* texel = &Texels[mips[0].tiledOffset + static_cast(y) * mips[0].pitchBytes + x * bytes]; + texel[0] = static_cast(Code(format, false, x)); + if (bytes == 2u) texel[1] = static_cast(Code(format, true, x)); + } + } +} + +void FillInput(bool gather, bool between) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t base = tid % (Size - 1u); + float u = (static_cast(tid % Size) + 0.5f) / static_cast(Size); + if (gather) u = (static_cast(base) + 0.6f) / static_cast(Size); + else if (between) u = static_cast(base + 1u) / static_cast(Size); + Input[tid * 3u + 0u] = u; + Input[tid * 3u + 1u] = 0.6f / static_cast(Size); + Input[tid * 3u + 2u] = 0.0f; + } +} + +bool Close(double actual, double expected) { + return std::fabs(actual - expected) <= 0.01 + std::fabs(expected) * 0.04; +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, float scale) { + std::vector userData(16, 0u); + const auto texture = TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize); + std::copy(texture.begin(), texture.end(), userData.begin() + 4); + std::uint32_t bits = 0; + std::memcpy(&bits, &scale, sizeof(bits)); + StoreCode[2] = bits; + const std::array memory{{{reinterpret_cast(StoreCode.data()), std::as_bytes(std::span(StoreCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(StoreCode.data()), std::span(StoreCode), 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(StoreCode.data())); + device.WaitIdle(); +} + +void CheckStored(AgcDriver::VulkanDevice& device, std::uint8_t* texels, float scale, const std::string& what) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + Input[tid * 3u + 0u] = (static_cast(tid) + 0.5f) / static_cast(StoreSize); + Input[tid * 3u + 1u] = 0.5f / static_cast(StoreSize); + Input[tid * 3u + 2u] = 0.0f; + } + std::vector userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize); + const auto sampler = SamplerDescriptor(FilterPoint); + 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); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(SampleCode.data()), std::as_bytes(std::span(SampleCode))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(SampleCode.data()), std::span(SampleCode), 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); + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(SampleCode.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const double code = std::round(std::min(1.0, static_cast(tid) * scale) * 255.0); + const double expected = Decode(static_cast(code)); + Require(Close(Output[tid], expected), what + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected)); + } +} + +void CheckStorageSource(AgcDriver::VulkanDevice& device) { + GuestBlock block; + auto* texels = block.Data(); + const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor(Srgb8, SwizzleX, texels, StoreSize))); + Require(surface.guestBytes >= (1u << 20u) && surface.guestBytes <= BlockBytes, "8_SRGB storage source: the surface does not fit the guest block or is below the storage image floor"); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), surface.guestBytes, nullptr, "test"); + AgcDriver::Graphics::FlushCachedTextures(device.Device()); + Store(device, texels, 1.0f / 32.0f); + CheckStored(device, texels, 1.0f / 32.0f, "8_SRGB sample of a pending storage image"); + const auto before = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), surface.guestBytes); + Require(before != nullptr, "8_SRGB storage source: no storage image holds the surface's results"); + const auto version = before->Version(); + Store(device, texels, 1.0f / 8.0f); + const auto after = AgcDriver::Graphics::StorageTexture::FindPending(reinterpret_cast(texels), surface.guestBytes); + Require(after != nullptr && after->Version() > version, "8_SRGB storage source: the second store did not advance the storage image"); + CheckStored(device, texels, 1.0f / 8.0f, "8_SRGB sample of a pending storage image after a second store"); +} + +void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, std::uint32_t filter, bool gather, const std::string& what) { + FillTexels(format); + const bool between = filter == FilterBilinear; + FillInput(gather, between); + const auto& code = gather ? GatherCode : SampleCode; + std::vector userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(sizeof(Output))); + const auto texture = TextureDescriptor(format, second ? SwizzleYAsX : SwizzleX); + const auto sampler = SamplerDescriptor(filter); + 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); + std::copy(sampler.begin(), sampler.end(), userData.begin() + 16); + const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(std::span(code))}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), std::span(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); + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t base = tid % (Size - 1u); + if (gather) { + const double near = Decode(Code(format, second, base)); + const double far = Decode(Code(format, second, base + 1u)); + const std::array expected{near, far, far, near}; + for (std::uint32_t component = 0; component < 4u; ++component) { + const double actual = Output[tid * 4u + component]; + Require(Close(actual, expected[component]), what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(actual) + ", expected " + std::to_string(expected[component])); + } + continue; + } + double expected = Decode(Code(format, second, tid % Size)); + if (between) expected = (Decode(Code(format, second, base)) + Decode(Code(format, second, base + 1u))) / 2.0; + const double actual = Output[tid]; + Require(Close(actual, expected), what + ": thread " + std::to_string(tid) + " sampled " + std::to_string(actual) + ", expected " + std::to_string(expected)); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + for (const auto format : {Srgb8, Srgb8_8}) { + const std::string name = format == Srgb8 ? "8_SRGB" : "8_8_SRGB"; + Check(*device, format, false, FilterPoint, false, name + " point sample"); + Check(*device, format, false, FilterBilinear, false, name + " bilinear sample, decoded before filtering"); + Check(*device, format, false, FilterPoint, true, name + " gather4"); + if (format == Srgb8_8) { + Check(*device, format, true, FilterPoint, false, name + " point sample of the second channel"); + Check(*device, format, true, FilterBilinear, false, name + " bilinear sample of the second channel, decoded before filtering"); + Check(*device, format, true, FilterPoint, true, name + " gather4 of the second channel"); + } + } + CheckStorageSource(*device); + std::puts("image sRGB sample copy tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From 97354b7fd15fe4105b67b0f864094ff20c38fb00 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:15:26 -0400 Subject: [PATCH 091/131] docs(debt): sRGB 8 and 8_8 textures read an RGBA8 sRGB copy --- docs/dev/debt/libSceAgcDriver.md | 2 +- docs/dev/debt/shader.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index f85f514f26..61cd50cbd7 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -89,7 +89,7 @@ - [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. - An [unnormalized S#](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) maps clamp mode 0 (repeat) on X and Y to clamp-to-edge as required by Vulkan unnormalized coordinates, and assigns the magnification filter when minification and magnification filters differ (since unnormalized coordinates sample at LOD 0). Anisotropy, TRUNC_COORD, MC_COORD_TRUNC and other clamp modes still throw. Secondary LOD bias (LOD_BIAS_SEC, bits [19:14]) is accepted and ignored, matching RDNA2 hardware measurements across explicit and implicit samples. High on Life (PPSA15388, #4736) uses secondary LOD bias and unnormalized coordinates with clamp mode 0 and differing min/mag filters during home base and city rendering. - [Background link-time-optimized relink of library pipelines](../../../core/libs/prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp) (libSceAgcDriver) is skipped on every GPU under NVIDIA's proprietary Vulkan driver (`VK_DRIVER_ID_NVIDIA_PROPRIETARY`), so those pipelines keep their fast-linked form. The optimized link loses the device when bound on an RTX 3080 (Ampere, #2866) and an RTX 4060 (Ada, #3434); an RTX 5060 and an RTX 5080 (Blackwell) on the same driver do not, so the scope is wider than the evidence. No standard Vulkan query tells the architecture apart, and the driver version that fixes it is unknown. `APS5_NO_PIPELINE_LTO` disables the relink on every driver. -- [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. +- [8_SRGB and 8_8_SRGB textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. A device that cannot sample them gets an RGBA8 sRGB copy instead (a blit of the R8 or RG8 texels, refreshed with the storage image's version), whose decode is likewise the device's; 3D textures and storage images that are not single-depth and format-matched are copied from their snapshot instead. - [PM4 submission rings](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp) (libSceAgcDriver) keep only the first 512 dwords of each submission, so the packets past them are not printed. The [failure report](../../../core/libs/prx/libc/src/FailureReport.cpp) dump does not allocate from the C runtime heap: packet names come from static strings, each line is formatted with integer and string conversions into a 512-byte stack buffer and written with one `WriteFile`. It still runs on whatever state the crash left. It waits about 100 yields for each ring's lock and otherwise prints that queue as busy, so a crash while recording a submission loses that queue's ring. It reads the rings without checking them, so a crash that corrupted them can fault inside the dump; such a fault during the report of an unhandled exception stops in that report's `Sleep(INFINITE)` and the process hangs instead of ending. The reports run at most once per process. Linux has no crash report: there the rings are printed only by `APS5_STALL_WATCHDOG_SECS`. - [WAIT_REG_MEM](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp) reports a diagnostic after `APS5_GPU_WAIT_TIMEOUT_MS` without treating the timeout as completion. An unsatisfied wait keeps polling and checking driver failures; a missing producer can therefore leave its queue waiting indefinitely. Existing recorded-label shortcuts remain in use. - [COND_EXEC](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 12e6133a6d..8387d4982f 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -62,8 +62,8 @@ - [`ds_wrxchg2_rtn_b32`, `ds_wrxchg2st64_rtn_b32`, `ds_wrxchg2_rtn_b64` and `ds_wrxchg2st64_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit or 64-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. - [`ds_condxchg32_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) uses two 32-bit atomic conditional exchanges. RDNA2 (gfx1035) measurements confirmed aligned return values, conditional bit clearing and 16-bit offsets; out-of-range accesses return zero without modifying LDS. Misaligned accesses fault the wave on gfx1035; the recompiler does not model this fault. Radeon 780M (gfx1103, RDNA3) measurements confirmed aligned return values, conditional bit clearing, EXEC masking and operand overlap in wave32 and wave64; contended cases passed per-dword conservation checks. Contended lane ordering and gfx1013 remain unmeasured; pair-wide indivisibility is not assumed, and concurrent 64-bit LDS atomics use a separate lock. GDS throws. VGPR pairs extending beyond v255 throw; allocated-VGPR EXEC nullification is not emulated. - [64-bit image atomics](../../../core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp) (`image_atomic_*` with a two-dword data mask) need `VK_EXT_shader_image_atomic_int64` and act on 32_32 UINT, SINT and FLOAT surfaces through an R64_UINT storage view. 64-bit `image_atomic_inc`, `image_atomic_dec` and the float atomics throw, as does any other access in the same shader to an image it updates with 64-bit atomics. -- [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. -- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures throw on a device that cannot sample R8_SRGB or R8G8_SRGB. +- [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. This is the path on a device that cannot blit R8 or RG8 to RGBA8, and with `APS5_SRGB_SHADER_DECODE=1`; otherwise a load of a sampled texture reads the RGBA8 sRGB copy and gets the device's own decode. +- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. - [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. - 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. From b5502ee4a101c491cbfb35662844180db004f6ae Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:20:04 -0400 Subject: [PATCH 092/131] fix(agc): rename the near and far locals that windows.h defines Co-authored-by: Jacmd <59657462+Jacmd@users.noreply.github.com> --- .../libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp index ed83b35bd8..15311eba5e 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp @@ -276,9 +276,9 @@ void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, s for (std::uint32_t tid = 0; tid < Threads; ++tid) { const std::uint32_t base = tid % (Size - 1u); if (gather) { - const double near = Decode(Code(format, second, base)); - const double far = Decode(Code(format, second, base + 1u)); - const std::array expected{near, far, far, near}; + const double low = Decode(Code(format, second, base)); + const double high = Decode(Code(format, second, base + 1u)); + const std::array expected{low, high, high, low}; for (std::uint32_t component = 0; component < 4u; ++component) { const double actual = Output[tid * 4u + component]; Require(Close(actual, expected[component]), what + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(actual) + ", expected " + std::to_string(expected[component])); From 192a37d026fd14621174116a941df5c645028ba2 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:20:45 -0400 Subject: [PATCH 093/131] test(agc): force the sRGB sample copy with APS5_SRGB_SHADER_DECODE=copy --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 8 +++++ .../Graphics/src/TextureFormat.cpp | 2 +- .../tests/execution/ImageSrgbSampleCopy.cpp | 33 +++++++++++++++++++ docs/dev/debt/shader.md | 2 +- 4 files changed, 43 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fdca183551..03292a4e12 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1828,6 +1828,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test(NAME agc_driver_image_srgb_sample_copy_forced COMMAND agc_driver_image_srgb_sample_copy_tests) + set_tests_properties(agc_driver_image_srgb_sample_copy_forced PROPERTIES + ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=copy" + SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_image_srgb_sample_copy_forced PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_shared_write_partial_collect COMMAND agc_shared_write_tests --partial-collect) set_tests_properties(agc_shared_write_partial_collect PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index e72bae96fc..c9617b896f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -117,7 +117,7 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat) { std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical) { const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); - const bool always = forced != nullptr && std::strcmp(forced, "1") == 0; + const bool always = forced != nullptr && (std::strcmp(forced, "1") == 0 || std::strcmp(forced, "copy") == 0); const auto sampled = [&](ShaderRecompiler::IrBufferFormat format) { VkFormatProperties properties{}; formatProperties(physical, ResolveTextureFormat(static_cast(format)), &properties); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp index 15311eba5e..4ea7cc7525 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbSampleCopy.cpp @@ -2,6 +2,7 @@ #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.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/TextureTiling.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libc/include/GuestArena.hpp" @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -246,6 +248,35 @@ void CheckStorageSource(AgcDriver::VulkanDevice& device) { CheckStored(device, texels, 1.0f / 8.0f, "8_SRGB sample of a pending storage image after a second store"); } +std::string ForcedMode() { + const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE"); + return forced == nullptr ? std::string{} : std::string{forced}; +} + +void VKAPI_PTR NativeSrgbFormatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) { + *properties = {}; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; +} + +void VKAPI_PTR NoSrgbFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) { + *properties = {}; + if (format == VK_FORMAT_R8_SRGB || format == VK_FORMAT_R8G8_SRGB) return; + properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT | VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_BLIT_DST_BIT; +} + +std::uint32_t SrgbBits() { + return AgcDriver::Graphics::SrgbDecodeFormats(NoSrgbFormatProperties, VK_NULL_HANDLE); +} + +void CheckSelection() { + const auto mode = ForcedMode(); + Require(SrgbBits() != 0u, "a device that cannot sample R8_SRGB or R8G8_SRGB selected no sRGB decode formats"); + const auto decode = AgcDriver::Graphics::SrgbDecodeFormats(NativeSrgbFormatProperties, VK_NULL_HANDLE); + const auto copy = AgcDriver::Graphics::SrgbCopyFormats(NativeSrgbFormatProperties, VK_NULL_HANDLE, decode); + const std::uint32_t expected = mode == "copy" ? SrgbBits() : 0u; + Require(copy == expected, "a device that samples R8_SRGB and R8G8_SRGB selected sRGB copy formats " + std::to_string(copy) + " with APS5_SRGB_SHADER_DECODE='" + mode + "', expected " + std::to_string(expected)); +} + void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, std::uint32_t filter, bool gather, const std::string& what) { FillTexels(format); const bool between = filter == FilterBilinear; @@ -296,8 +327,10 @@ void Check(AgcDriver::VulkanDevice& device, std::uint32_t format, bool second, s int main() { try { + CheckSelection(); const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; + if (ForcedMode() == "copy") Require((device->Target().srgbDecodeFormats & SrgbBits()) == 0u, "APS5_SRGB_SHADER_DECODE=copy: the device cannot blit R8 or RG8 to RGBA8, so 8 and 8_8 sRGB samples have no copy"); for (const auto format : {Srgb8, Srgb8_8}) { const std::string name = format == Srgb8 ? "8_SRGB" : "8_8_SRGB"; Check(*device, format, false, FilterPoint, false, name + " point sample"); diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 8387d4982f..00fa36d332 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -63,7 +63,7 @@ - [`ds_condxchg32_rtn_b64`](../../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) uses two 32-bit atomic conditional exchanges. RDNA2 (gfx1035) measurements confirmed aligned return values, conditional bit clearing and 16-bit offsets; out-of-range accesses return zero without modifying LDS. Misaligned accesses fault the wave on gfx1035; the recompiler does not model this fault. Radeon 780M (gfx1103, RDNA3) measurements confirmed aligned return values, conditional bit clearing, EXEC masking and operand overlap in wave32 and wave64; contended cases passed per-dword conservation checks. Contended lane ordering and gfx1013 remain unmeasured; pair-wide indivisibility is not assumed, and concurrent 64-bit LDS atomics use a separate lock. GDS throws. VGPR pairs extending beyond v255 throw; allocated-VGPR EXEC nullification is not emulated. - [64-bit image atomics](../../../core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp) (`image_atomic_*` with a two-dword data mask) need `VK_EXT_shader_image_atomic_int64` and act on 32_32 UINT, SINT and FLOAT surfaces through an R64_UINT storage view. 64-bit `image_atomic_inc`, `image_atomic_dec` and the float atomics throw, as does any other access in the same shader to an image it updates with 64-bit atomics. - [8_SRGB and 8_8_SRGB loads](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. This is the path on a device that cannot blit R8 or RG8 to RGBA8, and with `APS5_SRGB_SHADER_DECODE=1`; otherwise a load of a sampled texture reads the RGBA8 sRGB copy and gets the device's own decode. -- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. +- [Samples and gathers](../../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures read an RGBA8 sRGB copy of the texture (libSceAgcDriver, [Texture.cpp](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp)) on a device that cannot sample R8_SRGB or R8G8_SRGB but can blit R8 or RG8 to RGBA8, so they use that device's own decode. They still throw on a device that cannot blit, and with `APS5_SRGB_SHADER_DECODE=1`. `APS5_SRGB_SHADER_DECODE=copy` also takes the copy on a device that samples these formats natively, so the copy can be tested on any device that can blit R8 or RG8 to RGBA8; on a device that cannot blit, it behaves like `APS5_SRGB_SHADER_DECODE=1`. A multisampled or depth view of these formats gets no copy. - [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. - 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. From c81cfce7cf336eacdc3ee1a5d41348bde55c49f8 Mon Sep 17 00:00:00 2001 From: Dextroid17 <217507631+Dextroid17@users.noreply.github.com> Date: Sun, 18 Oct 2026 07:27:33 -0400 Subject: [PATCH 094/131] docs(environment): say APS5_SRGB_SHADER_DECODE=copy samples 8 and 8_8 sRGB through the copy --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 27bf42846e..b615c34335 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -338,7 +338,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_SPANNED_WAVE` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | spans a single-wave wave64 compute program over two full 32-lane subgroups | | `APS5_SPIRV_OPT` | string | `core/libs/prx/libSceAgcDriver/tests/SpirvOptimizer.cpp` | | selects the SPIR-V optimization pipeline: `full` restores the performance pipeline, `none` skips optimization | | `APS5_SRGB_ATTACHMENT_PROXY` | string | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | forces the sRGB color-attachment proxy format; `1` forces it | -| `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | forces shader sRGB decode for the sRGB 8-bit formats | +| `APS5_SRGB_SHADER_DECODE` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp` | | `1` forces shader sRGB decode for the sRGB 8-bit formats; `copy` does the same, except that 8 and 8_8 sRGB sample an RGBA8 sRGB copy when the device can blit to RGBA8 | | `APS5_STAGING_POOL_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/BufferPool.cpp` | | size in MiB of the device-local staging buffer pool; `0` disables the device tier | | `APS5_STALE_EVICT` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp` | | also evicts a proved-present dead image's pending layers on an access | | `APS5_STALL_WATCHDOG_SECS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/SubmissionRing.cpp` | 0 | seconds a queue may go without progress before the submission rings are dumped; 0 disables the watchdog, at most 86400 | From bab404e7af3df7c91aacae292c7fedeab669a2b7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:33:39 -0700 Subject: [PATCH 095/131] fix(shader): keep a shader's selected image-table records within the image and pair limits Since #6243, each read whose record offset the CPU evaluates gets its own selected table column, so a shader that reads one heap at many such keys plans one image per key. Past 64 keys that is refused with "image resource limit exceeded", or with "sampled image/sampler pair limit exceeded" when each read is sampled twice, where the shader planned within both limits before. After planning the table columns, the tracker now estimates the images and sampled pairs that collecting the program would add, keyed as AddImage and AddSampledPair key them, from the planned column sources and the direct handles. If either is over its limit, it plans again with the columns of one heap unselected, the heap with the most selected columns first, until both fit or no heap is left. Those reads then share one keyed column, as before #6243: the union of their key domains and the largest key. A shader within both limits plans exactly as before, from one planning pass. The estimate counts images exactly. A direct sampler is told apart by its handle's words and whether the sample adjusts, before the canonicalization collecting it applies, so the pair count can only be too high and errs toward merging. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 5 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 74 +++++++++ .../Optimization/src/ResourceTracker.cpp | 150 ++++++++++++++++-- 3 files changed, 213 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6a76bea34f..5c94edaa37 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1724,6 +1724,9 @@ if(BUILD_TESTING) endif() add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless) set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_selected_records_image_limit COMMAND agc_shader_memory_tests --selected-records-image-limit) + add_test(NAME agc_shader_selected_records_pair_limit COMMAND agc_shader_memory_tests --selected-records-pair-limit) + set_tests_properties(agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching) set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_type_zero_image_materialization COMMAND agc_shader_memory_tests --type-zero-image-materialization) @@ -1744,7 +1747,7 @@ if(BUILD_TESTING) add_test(NAME agc_shader_entry_lane_proof_budget COMMAND agc_shader_memory_tests --entry-lane-proof-budget) set_tests_properties(agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_shader_bindless 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:$") + set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_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_shared_write_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_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_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) 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") diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 7847b0d021..54c5316c9d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1799,6 +1799,70 @@ void verifyImageTableUnkeyedAccess() { } } +struct TableManyGuest { + std::array, 128> heap{}; + std::array output{}; + std::array srt{}; +}; + +std::vector TableManyKeyCode(std::uint32_t first, std::uint32_t second, bool twoSamplers) { + std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4080a00u, 0xfa000040u, 0x7e100280u}; + for (std::uint32_t read = 0; read < first + second; read++) { + code.insert(code.end(), {0xf4000400u, 0xfa000050u + read * 4u, 0x8f108510u, read < first ? 0xf42c0502u : 0xf42c0514u, 0x20000000u, 0xf09c0f08u, 0x00450400u, 0x06100908u}); + if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00650400u, 0x06100908u}); + } + code.insert(code.end(), {0xe0700000u, 0x80070800u, 0xbf810000u}); + return code; +} + +void verifySelectedRecordsWithinLimits(bool pairs) { + using namespace ShaderRecompiler; + namespace Abi = ImageTableAbi; + static TableTexture textures[4]; + alignas(4096) static TableManyGuest guest; + auto& heap = guest.heap; + for (std::uint32_t record = 0; record < heap.size(); record++) heap[record] = TableT(textures[record % 4u].bytes.data(), TableFormat8888UNorm, TableType2D, record + 1u); + const auto heapV = TableV(heap.data(), 32u, static_cast(heap.size())); + const auto outputV = TableV(guest.output.data(), 0u, 16u); + guest.srt = {}; + std::copy(heapV.begin(), heapV.end(), guest.srt.begin()); + guest.srt[8] = 0x92u; + std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); + std::copy(heapV.begin(), heapV.end(), guest.srt.begin() + 16); + for (std::uint32_t read = 0; read + 20u < guest.srt.size(); read++) guest.srt[20u + read] = read + 2u; + const auto address = static_cast(reinterpret_cast(guest.srt.data())); + const std::array userData{static_cast(address), static_cast(address >> 32u)}; + const auto columnOf = [](const IrResourcePlan& plan, const ImageResource& image) { return *plan.descriptorSources.at(image.source).tableColumn; }; + const auto selectedTables = [&](const IrResourcePlan& plan) { + return static_cast(std::count_if(plan.info.images.begin(), plan.info.images.end(), [&](const ImageResource& image) { return image.table != NoTable && columnOf(plan, image).selected; })); + }; + const std::string name = pairs ? "selected records within the pair limit" : "selected records within the image limit"; + const std::uint32_t limit = pairs ? ShaderInfo::MaxSampledPairs / 2u : ShaderInfo::MaxImages; + const auto fitting = TableManyKeyCode(limit, 0u, pairs); + const auto atLimit = GetResourcePlan(TableRequest(fitting, userData)); + require(atLimit->info.images.size() == limit && selectedTables(*atLimit) == limit && atLimit->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": reads that fit lost the per-key split").c_str()); + const auto past = TableManyKeyCode(limit + 1u, 0u, pairs); + const auto request = TableRequest(past, userData); + const auto plan = GetResourcePlan(request); + require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && plan->info.sampledPairs.size() == (pairs ? 2u : 1u), (name + ": reads past the limit were not merged into one table").c_str()); + const auto merged = columnOf(*plan, plan->info.images[0]); + require(!merged.selected && merged.selection == 0u && !merged.unkeyed && merged.keyDomains.size() == limit + 1u, (name + ": the merged table is not the union of the keys").c_str()); + const auto compiled = CompileTable(request); + const auto& tables = compiled.capture->snapshot.tables; + require(tables.tables.size() == 1u, (name + ": the snapshot does not hold one table").c_str()); + const auto& snapshot = tables.tables[0]; + const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); + for (const auto key : {2u, 2u + limit / 2u, 2u + limit}) { + require(key < snapshot.codes.size() && (snapshot.codes[key] & Abi::PoisonFlag) == 0u && tables.words.at(snapshot.codes[key]).dwords == heap[key], (name + ": a key does not bind its record").c_str()); + } + require(snapshot.codes.at(1) == outside && snapshot.codes.at(limit + 3u) == outside, (name + ": a key no read selects was bound").c_str()); + if (pairs) return; + const auto split = TableManyKeyCode(40u, 30u, false); + const auto twoHeaps = GetResourcePlan(TableRequest(split, userData)); + const auto unselected = std::find_if(twoHeaps->info.images.begin(), twoHeaps->info.images.end(), [&](const ImageResource& image) { return !columnOf(*twoHeaps, image).selected; }); + require(twoHeaps->info.images.size() == 31u && selectedTables(*twoHeaps) == 30u && unselected != twoHeaps->info.images.end() && columnOf(*twoHeaps, *unselected).keyDomains.size() == 40u, (name + ": the larger heap was not merged first, or the smaller one lost its split").c_str()); +} + void verifyImageTableSrtKey() { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; @@ -5900,6 +5964,16 @@ int main(int argc, char** argv) { std::cout << "Image table keys, maps, records and registration passed\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-image-limit") { + verifySelectedRecordsWithinLimits(false); + std::cout << "Selected image-table records past the image limit merge back into one table per heap\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-pair-limit") { + verifySelectedRecordsWithinLimits(true); + std::cout << "Selected image-table records past the sampled pair limit merge back into one table per heap\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--srt-read-patching") { verifySrtReadPatching(); std::cout << "SRT read patching replaces repeated and multiple uses\n"; diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index 73b74dad2a..524fddde19 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -658,7 +658,7 @@ private: column.address = address; column.foldedAddend = folded ? baseAddend : 0u; column.foldedCarry = folded ? carry : 0u; - if (!address && !sampler) { + if (!address && !sampler && std::ranges::find(m_unselectedHandles, &handle) == m_unselectedHandles.end()) { DescriptorSource direct; MakeSource(handle, width, false, false, direct); DescriptorSource selection; @@ -1292,6 +1292,118 @@ private: if (!enabled) { return; } + const auto sources = m_sources; + const auto columns = m_tableColumns; + const auto reads = m_tablePlanningReads; + const auto memory = m_tablePlanningMemory; + PlanTableColumnPass(); + while (!PlannedWithinLimits()) { + const auto group = LargestSelectedGroup(); + if (group.empty()) { + return; + } + m_unselectedHandles.insert(m_unselectedHandles.end(), group.begin(), group.end()); + m_sources = sources; + m_tableColumns = columns; + m_tablePlanningReads = reads; + m_tablePlanningMemory = memory; + PlanTableColumnPass(); + } + } + + bool PlannedWithinLimits() { + std::vector images; + std::vector> pairs; + std::vector directImages; + std::vector> directSamplers; + const auto direct = std::numeric_limits::max() / 2u; + for (auto& block : m_program.Blocks()) { + for (IrValue* inst : block->Instructions()) { + const auto imageInfo = ImageOpcodeInfoOf(inst->Opcode()); + if (inst->Opcode() == IrOpcode::ImageBvhIntersectRay || imageInfo.access == ImageAccess::None || imageInfo.resourceClass == ImageResourceClass::None || inst->ArgumentCount() == 0u) { + continue; + } + const auto flags = inst->Flags(); + if (flags.index >= m_program.Resources().memoryInfo.size()) { + continue; + } + const auto& memory = m_program.Resources().memoryInfo[flags.index]; + if (memory.kind != ResourceKind::Image || memory.planningOnly || IsTablePlanningMemory(flags.index)) { + continue; + } + const IrValue* handle = inst->Argument(0)->Resolve(); + const TableColumnPlan* column = FindTableColumn(*handle); + std::uint32_t source = 0; + if (column != nullptr) { + source = column->source; + } else { + const auto found = std::ranges::find_if(directImages, [&](const IrValue* other) { return SameHandle(*other, *handle); }); + source = direct + static_cast(found - directImages.begin()); + if (found == directImages.end()) directImages.push_back(handle); + } + const auto key = ImageKey(source, memory, inst->Opcode()); + const auto image = std::ranges::find_if(images, [&](const ImageResource& other) { return SameImageKey(other, key); }); + const auto imageIndex = static_cast(image - images.begin()); + if (image == images.end()) images.push_back(key); + if (!imageInfo.needsSampler || inst->ArgumentCount() < 2u) { + continue; + } + const IrValue* samplerHandle = inst->Argument(1)->Resolve(); + const TableColumnPlan* samplerColumn = FindTableColumn(*samplerHandle); + std::uint32_t sampler = 0; + if (samplerColumn != nullptr) { + sampler = samplerColumn->source; + } else { + const bool sampleAdjust = (memory.imageSampleFlags & RdnaImageSampleFlagAdjust) != 0; + const auto found = std::ranges::find_if(directSamplers, [&](const auto& other) { return other.second == sampleAdjust && SameHandle(*other.first, *samplerHandle); }); + sampler = direct + static_cast(found - directSamplers.begin()); + if (found == directSamplers.end()) directSamplers.emplace_back(samplerHandle, sampleAdjust); + } + if (std::ranges::find(pairs, std::pair{imageIndex, sampler}) == pairs.end()) pairs.emplace_back(imageIndex, sampler); + } + } + return images.size() <= ShaderInfo::MaxImages && pairs.size() <= ShaderInfo::MaxSampledPairs; + } + + std::vector LargestSelectedGroup() const { + const auto unselected = [](TableColumn column) { + column.selected = false; + column.selection = 0u; + return column; + }; + std::vector> groups; + for (const auto& plan : m_tableColumns) { + const auto& column = m_sources[plan.source].tableColumn; + if (!column.has_value()) { + continue; + } + const auto group = std::ranges::find_if(groups, [&](const std::vector& members) { return unselected(*m_sources[members.front()].tableColumn) == unselected(*column); }); + if (group == groups.end()) { + groups.push_back({plan.source}); + } else if (std::ranges::find(*group, plan.source) == group->end()) { + group->push_back(plan.source); + } + } + const std::vector* largest = nullptr; + for (const auto& group : groups) { + const bool selected = std::ranges::any_of(group, [&](std::uint32_t source) { return m_sources[source].tableColumn->selected; }); + if (selected && group.size() > 1u && (largest == nullptr || group.size() > largest->size())) { + largest = &group; + } + } + std::vector handles; + if (largest == nullptr) { + return handles; + } + for (const auto& plan : m_tableColumns) { + if (std::ranges::find(*largest, plan.source) != largest->end()) { + handles.push_back(plan.handle); + } + } + return handles; + } + + void PlanTableColumnPass() { IndexMemoryUsers(); const auto directImages = DirectImageHandles(); for (auto& block : m_program.Blocks()) { @@ -1576,13 +1688,29 @@ private: resource.scalar = resource.scalar || op == IrOpcode::ReadConstBuffer || memory.kind == ResourceKind::ScalarBuffer; } + static ImageResource ImageKey(std::uint32_t source, const MemoryInfo& memory, IrOpcode op) { + ImageResource image; + image.source = source; + image.resourceClass = ImageOpcodeInfoOf(op).resourceClass; + image.dimension = memory.imageDimension; + image.mipMode = image.resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; + image.depthCompare = (memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0; + image.r128 = memory.imageR128; + image.packed = memory.imagePacked; + image.byElements = memory.imageByElements; + image.byComponents = memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u; + return image; + } + + static bool SameImageKey(const ImageResource& image, const ImageResource& key) { + return image.source == key.source && image.resourceClass == key.resourceClass && image.dimension == key.dimension && image.mipMode == key.mipMode && image.depthCompare == key.depthCompare && image.r128 == key.r128 && image.packed == key.packed && image.byElements == key.byElements && image.byComponents == key.byComponents; + } + std::uint32_t AddImage(std::uint32_t source, const MemoryInfo& memory, IrOpcode op, std::uint32_t pc) { - const auto resourceClass = ImageOpcodeInfoOf(op).resourceClass; - const auto mip = resourceClass == ImageResourceClass::Storage && memory.imageHasMip ? ImageMipMode::DynamicStorage : ImageMipMode::None; - const bool depth = (memory.imageSampleFlags & RdnaImageSampleFlagCompare) != 0; + const auto key = ImageKey(source, memory, op); for (std::uint32_t i = 0; i < m_info.images.size(); i++) { auto& image = m_info.images[i]; - if (image.source == source && image.resourceClass == resourceClass && image.dimension == memory.imageDimension && image.mipMode == mip && image.depthCompare == depth && image.r128 == memory.imageR128 && image.packed == memory.imagePacked && image.byElements == memory.imageByElements && image.byComponents == (memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u)) { + if (SameImageKey(image, key)) { Merge(image, memory, op, pc); return i; } @@ -1590,18 +1718,9 @@ private: if (m_info.images.size() >= ShaderInfo::MaxImages) { return std::numeric_limits::max(); } - ImageResource image; + ImageResource image = key; image.singleMipPlaneBiasCompatible = m_program.Resources().stage == IrShaderStage::Pixel; - image.source = source; image.firstUsePc = pc; - image.resourceClass = resourceClass; - image.dimension = memory.imageDimension; - image.mipMode = mip; - image.depthCompare = depth; - image.r128 = memory.imageR128; - image.packed = memory.imagePacked; - image.byElements = memory.imageByElements; - image.byComponents = memory.imageByElements != 0u ? memory.dataDwords / memory.imageByElements : 0u; Merge(image, memory, op, pc); m_info.images.push_back(image); return static_cast(m_info.images.size() - 1); @@ -1848,6 +1967,7 @@ private: std::vector m_tableColumns; std::vector m_tablePlanningReads; std::vector m_tablePlanningMemory; + std::vector m_unselectedHandles; struct EdgeSelectorEntry { IrBlock* block = nullptr; std::vector edges; From 364fb665c1f417fe990fa08170efdd2b108e1f12 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 04:33:39 -0700 Subject: [PATCH 096/131] docs(shader): note that selected image-table records merge past the image and pair limits --- docs/dev/ARCHITECTURE.md | 2 +- docs/dev/debt/shader.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index 60e568cd98..90b516023e 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -136,7 +136,7 @@ flowchart LR A T# or S# that a shader loads from a buffer at a runtime offset (`key * stride + addend`, or `(key << n) & mask`) is not a direct descriptor. [`ResourceTracker.cpp`](../../core/shader/recompiler/Optimization/src/ResourceTracker.cpp) plans it as a table column (`TableColumn`): the heap V#'s descriptor source, the record stride and addend, the descriptor's offset in the record, and the key domains that may narrow it. Accesses with the same heap and record shape share one column. The shader keeps computing the offset and indexes a table binding: images use one variable-count `SAMPLED_IMAGE` array (`RuntimeAbi::ImageTableBinding`, in the fragment set when pipeline libraries split the sets), samplers a `SAMPLER` array (`Binding::SamplerTable`). Devices without `runtimeDescriptorArray`, `descriptorBindingPartiallyBound` and `descriptorBindingVariableDescriptorCount` refuse such shaders. -A T# whose eight dwords and record offset the SRT walker can evaluate is planned as a selected column (`TableColumn::selected`): the offset is kept as a descriptor source (`TableColumn::selection`) and the shader indexes entry 0 of a one-record table. The column stays a whole table when the shader may write the memory its offset is read from at run time: the buffer it is read from, or any memory when the read or a write goes through a pointer or a GPU-only descriptor. Samplers are not selected: an S# the walker can evaluate is a direct sampler. +A T# whose eight dwords and record offset the SRT walker can evaluate is planned as a selected column (`TableColumn::selected`): the offset is kept as a descriptor source (`TableColumn::selection`) and the shader indexes entry 0 of a one-record table. The column stays a whole table when the shader may write the memory its offset is read from at run time: the buffer it is read from, or any memory when the read or a write goes through a pointer or a GPU-only descriptor. Samplers are not selected: an S# the walker can evaluate is a direct sampler. When the selected columns would take a shader past `ShaderInfo::MaxImages` or `ShaderInfo::MaxSampledPairs`, `PlanTableColumns` plans the columns of one heap unselected again, the heap with the most selected columns first, until both fit. At every invocation, `TableSnapshotter` in [`ResourceMaterializer.cpp`](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) reads each column's records from guest memory into `ResourceSnapshot::tables`: heap base and size, the record count (at most `ImageTableAbi::MaxKeys`), one code per record (an index into the distinct descriptor words, or a poison code) and the ranges it read. A selected column evaluates its offset and reads only that record, as the scalar loads would: dwords past the heap read zeros, an unmapped record or an offset that carries past 32 bits is poison, and an offset the walker cannot read faults the table instead of throwing. Its heap base moves to the record, so the shader's fault address and the written-heap check cover that record alone. Any other column narrows to the records its keys select, plus record 0, when every access has a key domain: a key buffer of at most 256 positions (`APS5_IMAGE_TABLE_KEY_SCAN`), or a value the SRT walker evaluates from user data, SRT reads and constants (which, for an image, selects its record before it could narrow). A loop-carried, lane-reduced or masked key keeps the whole column, so the cost of an invocation grows with the column's records and its distinct T#s. The snapshot is part of the invocation, not of the compiled variant: a changed table or key changes neither the SPIR-V nor the variant's disk key. diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index c567f99417..f5929b04a6 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -33,7 +33,7 @@ - An [image table](../../../core/shader/recompiler/ImageTableAbi.hpp) entry that cannot be bound exactly reads zeros, and an active lane that selects it raises a BDA fault (reason 9) naming the record's address, words and reason; the dispatch's write-back reports the first one. - An [image table](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) snapshot holds at most 65536 keys per table, 65536 distinct images and 256 S# entries per shader; a key past a longer table's snapshot, or an entry over the budget, faults when selected. A selected column (a T# the SRT walker evaluates) holds its one record wherever it lies. - [Image tables](../../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) read by `s_load` through a 64-bit pointer hold the keys the key's known bits allow, at most 65536; a key past them, or an entry at a wrapping, unaligned or unmapped address, faults when selected. -- 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. +- 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. - 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. - `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 b40e0bfb80906ecea2f074be16f40d0990ca394f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 15:26:05 +0200 Subject: [PATCH 097/131] fix(libkernel): report the accept queue length for a listening socket's read event A read event on a listening socket took its data from FIONREAD, which a listening socket refuses on Linux, so kevent and sceKernelAddReadEvent reported 0 connections while saying one was ready. FreeBSD 11.4 gives a listening socket filt_solisten, which reports so_qlen, the completed connections waiting for accept. Ready now checks SO_ACCEPTCONN and on Linux reads a TCP listener's accept backlog from TCP_INFO, where tcp_get_info puts it in tcpi_unacked; with no host call for the count (Windows, macOS, a listener that is not TCP) it reports 1, the lower bound the readiness already implies. --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 17 +++++++++++++++++ docs/dev/debt/libkernel.md | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5b79792d75..799c7b8b9b 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -740,6 +741,20 @@ int GuestSockets::Stat(int descriptor, FileStat* status) { return 0; } +namespace { +std::int64_t ListenQueueLength(NativeSocket socket) { +#ifdef __linux__ + tcp_info info{}; + socklen_t length = sizeof(info); + if (getsockopt(socket, IPPROTO_TCP, TCP_INFO, &info, &length) == 0 && length >= offsetof(tcp_info, tcpi_unacked) + sizeof(info.tcpi_unacked)) + return info.tcpi_unacked; +#else + (void)socket; +#endif + return 1; +} +} + bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* eof) { std::shared_ptr socket; { @@ -766,6 +781,8 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e int queued = 0; if (*data > 0 && ::ioctl(socket->value, SIOCOUTQ, &queued) == 0) *data = std::max(0, *data - queued); #endif + } else if (Listening(*socket)) { + *data = ListenQueueLength(socket->value); } else { #ifdef _WIN32 unsigned long available = 0; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2eb1a7613f..baef0291c7 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -125,7 +125,7 @@ - [/dev/random and /dev/urandom](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled with host OS entropy (`getentropy`, `rand_s` on Windows); both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, `fstat`, `lseek` and `mmap` see the null device, and opening them for writing throws - [pthread_attr_getinheritsched, pthread_attr_setinheritsched, pthread_attr_getscope, pthread_attr_setscope, pthread_attr_getstackaddr](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) follow FreeBSD 11 libthr ([thr_attr.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_attr.c), [thr_create.c](https://github.com/freebsd/freebsd-src/blob/5ed7eb0d97ba4436218e810f61bd059acba984c2/lib/libthr/thread/thr_create.c)), not measured on the console: the scope defaults to `PTHREAD_SCOPE_SYSTEM` (2), only it and `PTHREAD_SCOPE_PROCESS` (0) are accepted and other values return `EINVAL`, and an inherit value other than `PTHREAD_INHERIT_SCHED` (4) or `PTHREAD_EXPLICIT_SCHED` (0) returns `ENOTSUP`. `pthread_attr_getstackaddr` returns the configured stack address pointer (null by default). A null output of `pthread_attr_getinheritsched` throws, since libthr writes through it unchecked. A thread keeps the scope it was created with, taken from the creating thread under `PTHREAD_INHERIT_SCHED`, and `pthread_attr_get_np` and `scePthreadAttrGet` report it, but every guest thread is a host thread at system scope, so process scope changes nothing - [Shared memory](../../../core/libs/prx/libkernel/File/src/SharedMemory.cpp) (libkernel) on Linux implements anonymous and named objects, bounded descriptor writes and seeks, and `shm_unlink` with effective read/write permission checks from FreeBSD 11. Named paths up to 1023 bytes use `/anyps5-shm-` plus SHA-1; collisions are not detected, and the namespace is shared across processes in the host mount namespace. Named creation applies the guest umask without changing the host mask. Linux procfs and `/dev/shm` are required. Operations use an advisory directory lock; external host operations bypass it, unrelated objects serialize, and descriptor operations require two spare host descriptors and compare device/inode with the initial descriptor identity on both the pinned-object and ordinary-file fallback paths. [Guest mappings](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) support shared and private mappings of these descriptors through the allocation tracker and pin a duplicate descriptor during validation and mapping; mapping requires one spare host descriptor; file-backed mappings use the write-watch scan fallback because aliases and descriptor writes bypass per-mapping tracking. Named objects and file-backed mappings on Windows remain unsupported. Invalid non-null name pointers, console behaviour and title compatibility remain unverified. -- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw +- [sceKernelAddReadEvent and sceKernelAddWriteEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel): a read event on a listening socket, here and in `kevent`, reports the number of completed connections waiting to be accepted in `data`, as FreeBSD 11.4's [`filt_solisten`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3384-L3392) reports `so_qlen`: on Linux a TCP listener's accept queue from `TCP_INFO` (`tcpi_unacked`, which Linux fills with the accept backlog for a listening socket), and elsewhere, or for a listener that is not TCP, 1 while a connection is waiting, as no host call reports that count. A write event reports the free space of the socket's send buffer in `data`, as FreeBSD 11.4's [`filt_sowrite`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/uipc_socket.c#L3358-L3382) reports `sbspace`: on Linux the host's `SO_SNDBUF` less the bytes `SIOCOUTQ` counts as queued, which include the host's per-packet overhead, so the space shrinks by somewhat more than the bytes sent. Windows has no query for that space and macOS is not wired up, so both report the whole `SO_SNDBUF`, which does not shrink as data is queued. Only guest sockets are supported; other descriptors and a low watermark above 1 throw - [close](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) and [setsockopt](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) reach a [libSceNet](../../../core/libs/prx/libSceNet/Export.cpp) socket through hooks libSceNet registers, as `select` polls them; the other libkernel socket calls do not. libSceNet numbers its sockets from 32 in its own table, so a CRT file descriptor and a socket can share a number; `close` then closes the socket. On the console both are one kernel descriptor space - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards. Guest scalar transfers, positioned transfers, vectored transfers and seeks share a position lock across duplicated descriptors; a vector holds it for the whole operation. Independently opened descriptors and other processes do not share that lock, so vector transfers over their overlapping file ranges are not coordinated. Handles that are not disk files return `ESPIPE`. A pipe or console `readv` can return a short read when no more input is available; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards; positioned transfers, plain transfers and seeks of one descriptor are serialized per descriptor (shared across duplicated descriptors), so a concurrent transfer observes a stable file pointer From 93ea8f2684ba4a93c7c4b75dbe609f52c8c21176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexej=20Szpev=C3=A1k?= Date: Sat, 17 Oct 2026 15:26:05 +0200 Subject: [PATCH 098/131] test(libkernel): check the queued connection count on a listening socket The listener read event now reports one connection after the first connect, and on Linux two once a second client is waiting, before both are accepted. --- core/libs/tests/GuestSocketEvents.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/tests/GuestSocketEvents.cpp b/core/libs/tests/GuestSocketEvents.cpp index 94bf0656db..92048bd727 100644 --- a/core/libs/tests/GuestSocketEvents.cpp +++ b/core/libs/tests/GuestSocketEvents.cpp @@ -73,8 +73,22 @@ int main() { Require(events[0].ident == static_cast(listener)); Require(events[0].filter == EVFILT_READ); Require(events[0].udata == &marker); + Require(events[0].data == 1); + const int second = socket_nid_postfix(2, 1, 0); + Require(second >= 0 && connect_nid_postfix(second, address.data(), address.size()) == 0); +#ifdef __linux__ + const auto queued = std::chrono::steady_clock::now() + std::chrono::seconds(1); + do { + Require(sceKernelWaitEqueue(eq, events.data(), events.size(), &count, &timeout) == SCE_OK); + Require(count == 1 && events[0].ident == static_cast(listener)); + } while (events[0].data != 2 && std::chrono::steady_clock::now() < queued); + Require(events[0].data == 2); +#endif const int accepted = accept_nid_postfix(listener, nullptr, nullptr); Require(accepted >= 0); + const int acceptedSecond = accept_nid_postfix(listener, nullptr, nullptr); + Require(acceptedSecond >= 0); + Require(close_nid_postfix(acceptedSecond) == 0 && close_nid_postfix(second) == 0); Require(sceKernelDeleteReadEvent(eq, listener) == SCE_OK); Require(sceKernelDeleteReadEvent(eq, listener) == SCE_KERNEL_ERROR_ENOENT); From 4d906b8c0469cad393f778a26cd6adef067a1994 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:35:59 +0000 Subject: [PATCH 099/131] test(shader): count direct images when selected records reach the image limit The selected-records limit tests only read through image tables, so nothing showed that the estimate counts direct images and samplers too. TableManyKeyCode can now also load T#s straight from the SRT and sample them, with the same samplers as the table reads and optionally a third sampler on the last one. agc_shader_selected_records_direct_image_limit: - 63 selected reads and 1 direct image, 64 images: the per-key split is kept (63 selected tables and the direct image). - 63 selected reads and 2 direct images, 65 images: over the limit only because of the direct images. Heap A's reads merge into one unselected table beside the 2 direct images (3 images, 3 pairs), and keys 2, 33 and 64 still bind their records. agc_shader_selected_records_direct_pair_limit: - 31 selected reads and 1 direct image, each with 2 samplers, 64 pairs: the split is kept. - The same with a third sampler on the direct image, 65 pairs: over the pair limit only because of the direct sampler. The reads merge into one table (2 images, 5 pairs), and keys 2, 17 and 32 still bind. With ResourceTracker.cpp as on main 9caecfd2d5, the over-limit cases fail with "image resource limit exceeded" and "sampled image/sampler pair limit exceeded", and the cases within the limits pass. --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 66 +++++++++++++++++-- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 5c94edaa37..da9deb75c8 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1726,7 +1726,9 @@ if(BUILD_TESTING) set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_selected_records_image_limit COMMAND agc_shader_memory_tests --selected-records-image-limit) add_test(NAME agc_shader_selected_records_pair_limit COMMAND agc_shader_memory_tests --selected-records-pair-limit) - set_tests_properties(agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + add_test(NAME agc_shader_selected_records_direct_image_limit COMMAND agc_shader_memory_tests --selected-records-direct-image-limit) + add_test(NAME agc_shader_selected_records_direct_pair_limit COMMAND agc_shader_memory_tests --selected-records-direct-pair-limit) + set_tests_properties(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 PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_srt_read_patching COMMAND agc_shader_memory_tests --srt-read-patching) set_tests_properties(agc_shader_srt_read_patching PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") add_test(NAME agc_shader_type_zero_image_materialization COMMAND agc_shader_memory_tests --type-zero-image-materialization) @@ -1747,7 +1749,7 @@ if(BUILD_TESTING) add_test(NAME agc_shader_entry_lane_proof_budget COMMAND agc_shader_memory_tests --entry-lane-proof-budget) set_tests_properties(agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") if(WIN32) - set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_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:$") + 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_shared_write_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_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_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) 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") diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 54c5316c9d..d754ef22fb 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -1805,17 +1805,28 @@ struct TableManyGuest { std::array srt{}; }; -std::vector TableManyKeyCode(std::uint32_t first, std::uint32_t second, bool twoSamplers) { +constexpr std::uint32_t TableManyDirectWord = 96u; +constexpr std::uint32_t TableManyExtraSamplerWord = 112u; + +std::vector TableManyKeyCode(std::uint32_t first, std::uint32_t second, bool twoSamplers, std::uint32_t direct = 0u, bool extraSampler = false) { std::vector code{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4080a00u, 0xfa000040u, 0x7e100280u}; + for (std::uint32_t image = 0; image < direct; image++) code.insert(code.end(), {0xf40c0000u | ((44u + image * 8u) << 6u), 0xfa000000u | ((TableManyDirectWord + image * 8u) * 4u)}); + if (extraSampler) code.insert(code.end(), {0xf4080f00u, 0xfa000000u | (TableManyExtraSamplerWord * 4u)}); for (std::uint32_t read = 0; read < first + second; read++) { code.insert(code.end(), {0xf4000400u, 0xfa000050u + read * 4u, 0x8f108510u, read < first ? 0xf42c0502u : 0xf42c0514u, 0x20000000u, 0xf09c0f08u, 0x00450400u, 0x06100908u}); if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00650400u, 0x06100908u}); } + for (std::uint32_t image = 0; image < direct; image++) { + const auto resource = ((44u + image * 8u) / 4u) << 16u; + code.insert(code.end(), {0xf09c0f08u, 0x00400400u | resource, 0x06100908u}); + if (twoSamplers) code.insert(code.end(), {0xf09c0f08u, 0x00600400u | resource, 0x06100908u}); + if (extraSampler && image + 1u == direct) code.insert(code.end(), {0xf09c0f08u, 0x01e00400u | resource, 0x06100908u}); + } code.insert(code.end(), {0xe0700000u, 0x80070800u, 0xbf810000u}); return code; } -void verifySelectedRecordsWithinLimits(bool pairs) { +void verifySelectedRecordsWithinLimits(bool pairs, bool direct) { using namespace ShaderRecompiler; namespace Abi = ImageTableAbi; static TableTexture textures[4]; @@ -1830,6 +1841,11 @@ void verifySelectedRecordsWithinLimits(bool pairs) { std::copy(outputV.begin(), outputV.end(), guest.srt.begin() + 12); std::copy(heapV.begin(), heapV.end(), guest.srt.begin() + 16); for (std::uint32_t read = 0; read + 20u < guest.srt.size(); read++) guest.srt[20u + read] = read + 2u; + for (std::uint32_t image = 0; image < 2u; image++) { + const auto direct = TableT(textures[image].bytes.data(), TableFormat8888UNorm, TableType2D, 200u + image); + std::copy(direct.begin(), direct.end(), guest.srt.begin() + TableManyDirectWord + image * 8u); + } + guest.srt[TableManyExtraSamplerWord] = 0x91u; const auto address = static_cast(reinterpret_cast(guest.srt.data())); const std::array userData{static_cast(address), static_cast(address >> 32u)}; const auto columnOf = [](const IrResourcePlan& plan, const ImageResource& image) { return *plan.descriptorSources.at(image.source).tableColumn; }; @@ -1838,6 +1854,37 @@ void verifySelectedRecordsWithinLimits(bool pairs) { }; const std::string name = pairs ? "selected records within the pair limit" : "selected records within the image limit"; const std::uint32_t limit = pairs ? ShaderInfo::MaxSampledPairs / 2u : ShaderInfo::MaxImages; + const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); + if (direct) { + const auto directImages = [&](const IrResourcePlan& plan) { + return static_cast(std::count_if(plan.info.images.begin(), plan.info.images.end(), [](const ImageResource& image) { return image.table == NoTable; })); + }; + const auto reads = limit - 1u; + const auto fittingDirectCode = TableManyKeyCode(reads, 0u, pairs, 1u); + const auto fittingDirect = GetResourcePlan(TableRequest(fittingDirectCode, userData)); + const auto describe = [&](const IrResourcePlan& plan) { + return std::to_string(plan.info.images.size()) + " images (" + std::to_string(selectedTables(plan)) + " selected tables, " + std::to_string(directImages(plan)) + " direct), " + std::to_string(plan.info.sampledPairs.size()) + " pairs"; + }; + require(fittingDirect->info.images.size() == limit && selectedTables(*fittingDirect) == reads && directImages(*fittingDirect) == 1u && fittingDirect->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": selected reads and a direct image that fit lost the per-key split").c_str()); + std::cout << name << ": " << reads << " selected reads and 1 direct image" << (pairs ? ", each with 2 samplers" : "") << ": " << describe(*fittingDirect) << '\n'; + const auto pastDirectCode = TableManyKeyCode(reads, 0u, pairs, pairs ? 1u : 2u, pairs); + const auto pastDirectRequest = TableRequest(pastDirectCode, userData); + const auto pastDirect = GetResourcePlan(pastDirectRequest); + const auto mergedDirect = std::find_if(pastDirect->info.images.begin(), pastDirect->info.images.end(), [](const ImageResource& image) { return image.table != NoTable; }); + require(pastDirect->info.images.size() == (pairs ? 2u : 3u) && selectedTables(*pastDirect) == 0u && directImages(*pastDirect) == (pairs ? 1u : 2u) && pastDirect->info.sampledPairs.size() == (pairs ? 5u : 3u) && mergedDirect != pastDirect->info.images.end(), (name + ": selected reads that the direct " + (pairs ? "sampler" : "images") + " push past the limit were not merged into one table").c_str()); + const auto mergedColumn = columnOf(*pastDirect, *mergedDirect); + require(!mergedColumn.selected && mergedColumn.selection == 0u && !mergedColumn.unkeyed && mergedColumn.keyDomains.size() == reads, (name + ": the table merged beside the direct " + (pairs ? "sampler" : "images") + " is not the union of the keys").c_str()); + const auto compiledDirect = CompileTable(pastDirectRequest); + const auto& directTables = compiledDirect.capture->snapshot.tables; + require(directTables.tables.size() == 1u, (name + ": the snapshot beside the direct " + (pairs ? "sampler" : "images") + " does not hold one table").c_str()); + const auto& directSnapshot = directTables.tables[mergedDirect->table]; + for (const auto key : {2u, 2u + reads / 2u, 1u + reads}) { + require(key < directSnapshot.codes.size() && (directSnapshot.codes[key] & Abi::PoisonFlag) == 0u && directTables.words.at(directSnapshot.codes[key]).dwords == heap[key], (name + ": a key beside the direct " + (pairs ? "sampler" : "images") + " does not bind its record").c_str()); + } + require(directSnapshot.codes.at(1) == outside && directSnapshot.codes.at(reads + 2u) == outside, (name + ": a key no read selects was bound beside the direct " + (pairs ? "sampler" : "images")).c_str()); + std::cout << name << ": " << reads << " selected reads and " << (pairs ? "1 direct image with a third sampler" : "2 direct images") << ": " << describe(*pastDirect) << ", the reads merged into one table with " << mergedColumn.keyDomains.size() << " key domains\n"; + return; + } const auto fitting = TableManyKeyCode(limit, 0u, pairs); const auto atLimit = GetResourcePlan(TableRequest(fitting, userData)); require(atLimit->info.images.size() == limit && selectedTables(*atLimit) == limit && atLimit->info.sampledPairs.size() == (pairs ? 2u : 1u) * limit, (name + ": reads that fit lost the per-key split").c_str()); @@ -1851,7 +1898,6 @@ void verifySelectedRecordsWithinLimits(bool pairs) { const auto& tables = compiled.capture->snapshot.tables; require(tables.tables.size() == 1u, (name + ": the snapshot does not hold one table").c_str()); const auto& snapshot = tables.tables[0]; - const auto outside = Abi::PoisonCode(static_cast(Abi::PoisonReason::OutsideDomain)); for (const auto key : {2u, 2u + limit / 2u, 2u + limit}) { require(key < snapshot.codes.size() && (snapshot.codes[key] & Abi::PoisonFlag) == 0u && tables.words.at(snapshot.codes[key]).dwords == heap[key], (name + ": a key does not bind its record").c_str()); } @@ -5965,15 +6011,25 @@ int main(int argc, char** argv) { return 0; } if (argc == 2 && std::string_view(argv[1]) == "--selected-records-image-limit") { - verifySelectedRecordsWithinLimits(false); + verifySelectedRecordsWithinLimits(false, false); std::cout << "Selected image-table records past the image limit merge back into one table per heap\n"; return 0; } if (argc == 2 && std::string_view(argv[1]) == "--selected-records-pair-limit") { - verifySelectedRecordsWithinLimits(true); + verifySelectedRecordsWithinLimits(true, false); std::cout << "Selected image-table records past the sampled pair limit merge back into one table per heap\n"; return 0; } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-direct-image-limit") { + verifySelectedRecordsWithinLimits(false, true); + std::cout << "Direct images count toward the image limit beside selected image-table records\n"; + return 0; + } + if (argc == 2 && std::string_view(argv[1]) == "--selected-records-direct-pair-limit") { + verifySelectedRecordsWithinLimits(true, true); + std::cout << "Direct samplers count toward the sampled pair limit beside selected image-table records\n"; + return 0; + } if (argc == 2 && std::string_view(argv[1]) == "--srt-read-patching") { verifySrtReadPatching(); std::cout << "SRT read patching replaces repeated and multiple uses\n"; From 7b3098714ea9aca276e414e364987e838263fd7d Mon Sep 17 00:00:00 2001 From: Alex Collini Date: Sun, 18 Oct 2026 13:42:27 +0200 Subject: [PATCH 100/131] docs(agc): keep the title name out of the released-textures debt entry 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 6c7838b646..70e0cc2887 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -172,7 +172,7 @@ - [Flip packets raise the graphics end-of-pipe event](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) (libSceAgcDriver) - a flip delivers the queue's end-of-pipe event (`sceAgcDriverAddEqEvent`) once the work recorded before it completes, as a `RELEASE_MEM` with `INT_SEL` does. Minecraft (PPSA17221) registers the graphics event, never requests an interrupt in its releases, and waits on the event after every flip while holding its render lock, so without this it never presents a second frame. Whether the console raises the event from the flip itself or from an interrupt its flip sequence contains is not measured; a title that waits only on release interrupts sees one extra event per flip, with `data` counting it. - [Host import windows and budget](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver): a registered range larger than 64 MiB is imported one 64 MiB window at a time for buffer and fill accesses; an access across window edges imports the cells it touches, grown to the union of the range's imports it overlaps. An address-based build (`AcquireRegistered`) imports the range whole and replaces its windows. Live imports stay under a budget: `APS5_HOST_IMPORT_MIB`, else the larger of 6 GiB and half the physical memory, at most the largest host heap less 2 GiB (imports and type-3 mirrors share it; #167's rule). A new import beyond it retires the least recently used imports not used in the last 256 lookups; one that would not fit even then is refused before anything is retired, and imports made when a range is mapped never evict. A refused import is tried again at its next use. The 64 MiB window, the 256-lookup protection and the 2 GiB reserve are choices, not measured. - [Registration preparation at submit](../../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (libSceAgcDriver) - `Driver::Submit` waits for every registration-time shader preparation queued before the call, not only those of the shaders the commands use, so a submission behind a long queue of unrelated preparations waits for all of them on the submitting thread. Waiting there instead of in the queue worker keeps the GPU side of a submission short: a title that sees a submission take seconds treats the GPU as hung (Goat Simulator 3, PPSA04159, then stops submitting and its garbage collector fails a 10 s timeout). On the console shaders are compiled ahead of time, so neither wait exists; how long a title tolerates a slow submit call is not known. -- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured only on Ghost of Yōtei (PPSA26344) on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. +- [Resource cache entries holding released textures](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a sampled texture the texture cache drops while something else holds it, or makes without caching (`admitTexture` refusing a first use), is marked released, and `ResourceCache::Insert` refuses descriptor sets that hold one and removes the entries that do. The removal waits for the next insert: until then `Find` still returns those entries and their textures stay alive. One process-wide flag starts the sweep, so while the sampled cache is over its budget every insert scans all entries (1,024 at start, up to 16,384 after growth, or `APS5_RESOURCE_CACHE_ENTRIES`, each with its textures); that cost is not measured. When `ReleaseTextureCaches` runs on device memory exhaustion, the sampled textures these entries hold are only marked released and are freed at the next insert, so the immediate allocation retry gets no memory back from them. A draw or dispatch whose textures the cache refused builds its descriptor sets again on each use for as long as the cache stays full; the count bounds (`APS5_RESOURCE_CACHE_ENTRIES`, `APS5_RESOURCE_CACHE_TABLE_ELEMENTS`) still bound nothing by bytes, and sampled textures made outside the texture cache (the disabled cache, depth-surface copies) are never marked. Measured on one title on one RTX 4090: with the default bounds, sampled textures held outside the cache peaked at 2,836 MiB (7,830 MiB before) and device-local heap 0 stayed under its 22.9 GB budget for the whole run (26,357 MiB before, which aborted with `VK_ERROR_OUT_OF_DEVICE_MEMORY` at the wolf scene). Whether the extra descriptor-set builds cost frame time elsewhere is not measured. - [Unclamped normalized color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - CB_COLOR_INFO targets with BLEND_CLAMP clear (bit 15) on UNORM and SNORM formats are accepted when blending is disabled (ENABLE clear or BLEND_BYPASS set). Vulkan converts unblended shader values and clamps to the normalized range automatically; blending into an unclamped normalized target remains unmodeled because Vulkan always clamps inputs prior to blending. - [Thick SW_4KB_S_X volumes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp) use AMD addrlib's GFX10 non-RB+ 16-pipe `GFX10_SW_4K_S3_X_PATINFO` equations for 1, 2, 4, 8 and 16-byte elements, including packed mip tails. The layout follows [AMD PAL c5e80007](https://github.com/GPUOpen-Drivers/pal/blob/c5e800072a32f68b6ccc4422936d96167c6e0728/src/core/imported/addrlib/src/gfx10/gfx10addrlib.cpp); it has not been measured on console hardware. Other thick swizzle modes outside the supported families still throw. Mipmapped 3D color targets remain refused by the color-register decoder. From ca7ea10d30ee8bf1e883dc9a59a6b497d163b10a Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Sun, 18 Oct 2026 05:09:14 -0700 Subject: [PATCH 101/131] docs(environment): add the rows for the performance overlay switches --- docs/dev/ENVIRONMENT.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 27bf42846e..70584350e4 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -65,6 +65,8 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| | `ANYPS5_GPU` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | restricts physical device selection to device names containing this case-insensitive substring | +| `ANYPS5_OVERLAY` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | 0 | 1 shows the performance overlay; 0 or unset hides it, and any other value is refused | +| `ANYPS5_OVERLAY_POS` | string | `core/libs/prx/libSceAgcDriver/Execution/src/PerformanceOverlay.cpp` | tl | the overlay's corner: tl, tr, bl or br | | `ANYPS5_PRESENT_MODE` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | fifo | selects the swapchain present mode: fifo, fifo_latest_ready, mailbox or immediate | | `ANYPS5_REQUIRE_DESCRIPTOR_BINDING_FLAGS` | presence | `core/libs/prx/libSceAgcDriver/tests/ComputePipelineReplay.cpp` | | fails the replay test instead of accepting a device that refuses descriptor binding flags | | `ANYPS5_REQUIRE_ROBUST2` | presence | `core/libs/prx/libSceAgcDriver/tests/execution/LdsMixedWidthAtomics.cpp` | | fails the LDS lock-budget test when the device does not enable robustBufferAccess2, instead of only printing it | From 2d96ff77a825c3fa926588d35482a305f5f9f733 Mon Sep 17 00:00:00 2001 From: ivica00 Date: Sun, 18 Oct 2026 14:13:08 +0200 Subject: [PATCH 102/131] fix(agc): drain DMA_DATA unless APS5_PIPELINE_DMA is set Co-authored-by: Cursor --- .../Execution/src/Driver/Packets/PacketExecution.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 52d75889a3..55ec0fe58d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -23,6 +23,11 @@ bool orderedFills() { return ordered; } +bool pipelineDma() { + static const bool enabled = std::getenv("APS5_PIPELINE_DMA") != nullptr; + return enabled; +} + bool keepsDrawPipeline(std::span packet, std::uint32_t header, std::uint32_t opcode) { if (header == FlipPacketHeader || header == RenderingWaitPacketHeader) return false; if (Pm4::DrawOpcode(opcode)) return true; @@ -33,7 +38,7 @@ bool keepsDrawPipeline(std::span packet, std::uint32_t head case 0x63: case 0x64: case 0x9f: return packet.size() >= 5 && !DrawPipeline::Queue0().Overlaps(packet[1] | (static_cast(packet[2]) << 32u), static_cast(packet[4]) * 8); case 0x50: { - if (packet.size() < 7) return false; + if (!pipelineDma() || packet.size() < 7) return false; const auto destination = Pm4::DmaDestination(packet); if (destination == Pm4::DmaSelectGds || destination == Pm4::DmaDestinationNowhere) return true; const auto bytes = Pm4::DmaBytes(packet); From afe2141a7cca4d85ca0a062203eb7599db4b8781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 18 Oct 2026 14:40:02 +0200 Subject: [PATCH 103/131] ci(pages): deploy only when the site or the progress changes Co-Authored-By: Claude --- .github/workflows/pages-progress.yml | 40 ++++++++++++++++++++++++++++ .github/workflows/pages.yml | 11 ++++++++ docs/dev/PROGRESS.md | 2 +- 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pages-progress.yml diff --git a/.github/workflows/pages-progress.yml b/.github/workflows/pages-progress.yml new file mode 100644 index 0000000000..88c1f81d5a --- /dev/null +++ b/.github/workflows/pages-progress.yml @@ -0,0 +1,40 @@ +name: Pages progress + +on: + push: + branches: [main] + paths: + - core/libs/prx/** + - core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp + +permissions: + contents: read + actions: write + pages: read + +concurrency: + group: pages-progress + cancel-in-progress: true + +jobs: + progress: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Deploy Pages if the progress changed + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: | + python3 tools/progress.py "$RUNNER_TEMP/progress" + site=$(gh api "repos/$GH_REPO/pages" --jq .html_url) + if curl -fsSL "${site%/}/progress.json?$GITHUB_SHA" -o "$RUNNER_TEMP/deployed.json" && + cmp -s "$RUNNER_TEMP/deployed.json" "$RUNNER_TEMP/progress/progress.json"; then + echo "Progress unchanged, Pages not deployed" >> "$GITHUB_STEP_SUMMARY" + else + gh workflow run pages.yml --ref main + echo "Progress changed, Pages deployment dispatched" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d789d4de62..29891c5c96 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -3,6 +3,17 @@ name: Pages on: push: branches: [main] + paths: + - .github/workflows/pages.yml + - CMakeLists.txt + - core/relinker/** + - demo/web/** + - tools/build_web.py + - tools/nid_names.py + - tools/progress.py + - tools/progress_history.py + - tools/rdna_isa.txt + - tools/test_web.py workflow_dispatch: release: types: [published] diff --git a/docs/dev/PROGRESS.md b/docs/dev/PROGRESS.md index e66bf9fcad..b337ab4c98 100644 --- a/docs/dev/PROGRESS.md +++ b/docs/dev/PROGRESS.md @@ -1,6 +1,6 @@ # Progress reporting -[`tools/progress.py`](../../tools/progress.py) generates `progress.json`, HTML, SVG maps and badges. The [Progress workflow](../../.github/workflows/progress.yml) publishes them after pushes to `main`. The [Progress report workflow](../../.github/workflows/progress-report.yml) compares the base and head trees of pull requests. +[`tools/progress.py`](../../tools/progress.py) generates `progress.json`, HTML, SVG maps and badges. The [Pages workflow](../../.github/workflows/pages.yml) publishes them with the web converter after pushes to `main` that change the converter, the web page or these tools. The [Pages progress workflow](../../.github/workflows/pages-progress.yml) runs after pushes that change `core/libs/prx` or the opcode list, compares the generated `progress.json` with the deployed one and dispatches the Pages workflow only when they differ, so the history chart gains new days only when the progress changes. The [Progress report workflow](../../.github/workflows/progress-report.yml) compares the base and head trees of pull requests. ## Library counts From a2e79fb3fdafdb0eb772624223c981283a4fe755 Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 17:37:39 -0400 Subject: [PATCH 104/131] fix(shader): bind an unnormalized S# normalized when it samples only null images An S# that forces unnormalized coordinates was refused when it samples a T# the shader declares 3D, 2D-array, cube or multisampled, since Vulkan allows unnormalized coordinates only with a single-level 1D or 2D view. A null T# reads zeros at any coordinate, and a T# whose channels all select constants is emitted as those constants without a sample, so when the S# samples no other T# its coordinates cannot change a result. Such an S# is now marked in its binding (samplerCoordinatesIgnored) and the driver creates its sampler without FORCE_UNNORMALIZED. The flag is part of the binding content key and of the shader disk cache, whose format version is raised to 38. An S# that also samples a real T# while a null T# of such a shape shares it is still refused. PPSA01317 pairs an unnormalized clamped bilinear S# with a null 3D T# and a 3D T# whose channels all select 0 for image_sample_lz in a compute shader during boot. AI-assisted: yes --- .../Graphics/src/ShaderResources.cpp | 14 +++++-- .../libSceAgcDriver/tests/ShaderDiskCache.cpp | 3 +- .../libSceAgcDriver/tests/ShaderMemory.cpp | 42 +++++++++++++++++++ .../Optimization/ResourceMaterializer.hpp | 1 + .../src/DescriptorBindingBuilder.cpp | 19 ++++++++- .../Optimization/src/ResourceMaterializer.cpp | 4 ++ core/shader/recompiler/Recompiler.hpp | 1 + core/shader/recompiler/ShaderDiskCache.cpp | 2 + core/shader/recompiler/ShaderDiskCache.hpp | 2 +- 9 files changed, 81 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b8c914b8f4..95628d1238 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -2347,6 +2347,7 @@ void VisitContentKey(const CompiledShader& shader, bool dataWords, bool movableB packBits(binding.imageAtomic); packBits(binding.samplerUnnormalized); packBits(binding.samplerIntegerBorder); + packBits(binding.samplerCoordinatesIgnored); packBits(binding.imageUnnormalized); packBits(binding.imageUnnormalizedBaseLevel); // Read-only elements are bound without a write set: an object built for one written set @@ -2368,11 +2369,11 @@ std::vector ShaderResources::ContentKey(const CompiledShader& sha words += count; }; for (const auto& binding : shader.program->bindings) { - addWords(19); + addWords(20); addWords(binding.imageSamplers.size()); addWords(binding.imageShapes.size()); if ((dataWords && !movableBuffers) || !DataRole(binding.role)) addWords(binding.guestDescriptor.size()); - for (const auto* bits : {&binding.imageWritten, &binding.samplerDepthCompare, &binding.imageDepthCompare, &binding.imageAtomic, &binding.samplerUnnormalized, &binding.samplerIntegerBorder, &binding.imageUnnormalized, &binding.imageUnnormalizedBaseLevel, &binding.bufferWritten}) addWords(bits->size() / 32u + (bits->size() % 32u != 0 ? 1u : 0u)); + for (const auto* bits : {&binding.imageWritten, &binding.samplerDepthCompare, &binding.imageDepthCompare, &binding.imageAtomic, &binding.samplerUnnormalized, &binding.samplerIntegerBorder, &binding.samplerCoordinatesIgnored, &binding.imageUnnormalized, &binding.imageUnnormalizedBaseLevel, &binding.bufferWritten}) addWords(bits->size() / 32u + (bits->size() % 32u != 0 ? 1u : 0u)); } key.reserve(words); VisitContentKey(shader, dataWords, movableBuffers, [&](std::uint32_t word) { key.push_back(word); }); @@ -4082,7 +4083,14 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding& Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata"); Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size"); for (std::uint32_t element = 0; element < binding.count; ++element) { - const auto words = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + auto guestWords = std::span(binding.guestDescriptor).subspan(static_cast(element) * elementWords, elementWords); + std::array normalizedWords{}; + if (element < binding.samplerCoordinatesIgnored.size() && binding.samplerCoordinatesIgnored[element]) { + std::copy(guestWords.begin(), guestWords.end(), normalizedWords.begin()); + normalizedWords[0] &= ~(1u << 15u); + guestWords = normalizedWords; + } + const auto words = guestWords; const bool compareEnable = binding.samplerDepthCompare.at(element); const bool unnormalized = element < binding.samplerUnnormalized.size() && binding.samplerUnnormalized[element]; const bool integerBorder = element < binding.samplerIntegerBorder.size() && binding.samplerIntegerBorder[element]; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index e8f3b6a3ce..6c9257ac77 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -84,7 +84,7 @@ void removeCacheDirectory(const std::filesystem::path& directory) { } bool sameBinding(const DescriptorBinding& left, const DescriptorBinding& right) { - return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.imageShapes == right.imageShapes && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers; + return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.imageShapes == right.imageShapes && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.bufferRead == right.bufferRead && left.samplerUnnormalized == right.samplerUnnormalized && left.samplerIntegerBorder == right.samplerIntegerBorder && left.samplerCoordinatesIgnored == right.samplerCoordinatesIgnored && left.imageUnnormalized == right.imageUnnormalized && left.imageUnnormalizedBaseLevel == right.imageUnnormalizedBaseLevel && left.imageSamplers == right.imageSamplers; } bool sameBindings(const std::vector& left, const std::vector& right) { @@ -176,6 +176,7 @@ DescriptorBinding sampleBinding(std::uint32_t seed) { binding.bufferWritten = {false, false, true, true, false}; binding.samplerUnnormalized = {false, true, true}; binding.samplerIntegerBorder = {true, false, seed % 2 == 1}; + binding.samplerCoordinatesIgnored = {false, seed % 2 == 0, true}; binding.imageUnnormalized = {true, false, seed % 2 == 0}; binding.imageUnnormalizedBaseLevel = {false, true, seed % 3 == 0}; binding.imageSamplers = {0x5u, 0u, 0x80000000u}; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 7847b0d021..20860dd912 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4880,6 +4880,47 @@ void verifyUnnormalizedSamplers() { require(proven(pixelOffsetNormalized, false) && implicitMasks.size() == 1u && (implicitMasks[0] & (spv::ImageOperandsConstOffsetMask | spv::ImageOperandsOffsetMask)) != 0u, "unnormalized texel offsets: a normalized pixel image_sample_o is not an implicit-LOD sample with its offset"); } +void verifyNullVolumeUnnormalizedSampler() { + using namespace ShaderRecompiler; + ImageResource volume{}; + volume.resourceClass = ImageResourceClass::Sampled; + volume.numericClass = IrTextureNumericClass::Float; + volume.dimension = RdnaImageDimension::Dim3D; + volume.read = true; + ImageResource plane = volume; + plane.dimension = RdnaImageDimension::Dim2D; + SamplerResource sampler{}; + sampler.uses = SamplerUseExplicitLod; + const DescriptorValue nullDescriptor{{0u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}, 8u}; + const DescriptorValue volumeDescriptor{{0x00001000u, 0x03800000u, 0x0000c000u, 0xa0000facu, 3u, 0u, 0u, 0u}, 8u}; + const DescriptorValue planeDescriptor{{0x00002000u, 0x03800000u, 0x0000c000u, 0x90000facu, 0u, 0u, 0u, 0u}, 8u}; + const DescriptorValue unnormalized{{0x00008092u, 0x00fff000u, 0x02500000u, 0u}, 4u}; + const auto populate = [&](std::vector images, std::vector descriptors) { + ShaderInfo info; + info.images = images; + info.samplers = {sampler}; + for (std::uint32_t index = 0; index < images.size(); ++index) { + info.sampledPairs.push_back({index, 0u, 0u}); + info.runtimeImageModes.push_back(ResourceMaterializer::RuntimeImageModes(images[index])); + } + ResourceSnapshot snapshot; + snapshot.images = descriptors; + snapshot.samplers = {unnormalized}; + BindingAllocationResult allocation; + for (std::uint32_t index = 0; index < images.size(); ++index) allocation.layout.descriptors.push_back({DescriptorBindingForImage(images[index]), {index}}); + allocation.layout.descriptors.push_back({DescriptorBindingKind::Samplers, {0u}}); + DescriptorBindingBuilder{}.Populate(allocation, info, IrShaderStage::Compute, 0u, snapshot, {}); + return allocation.bindings.back(); + }; + const auto nullOnly = populate({volume}, {nullDescriptor}); + require(nullOnly.samplerUnnormalized == std::vector{false} && nullOnly.samplerCoordinatesIgnored == std::vector{true}, "unnormalized samplers: an S# paired only with a null 3D T# was refused or kept its unnormalized coordinates"); + const DescriptorValue zeroSwizzle{{0x00003000u, 0x00100000u, 0u, 0xa0000000u, 0u, 0u, 0u, 0u}, 8u}; + const auto constantAndNull = populate({volume, volume}, {zeroSwizzle, nullDescriptor}); + require(constantAndNull.samplerUnnormalized == std::vector{false} && constantAndNull.samplerCoordinatesIgnored == std::vector{true}, "unnormalized samplers: an S# paired with a null 3D T# and a 3D T# whose channels all select constants was refused or kept its unnormalized coordinates"); + expectFailure([&] { static_cast(populate({volume}, {volumeDescriptor})); }, "samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted"); + expectFailure([&] { static_cast(populate({volume, plane}, {nullDescriptor, planeDescriptor})); }, "samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: an S# shared by a null 3D T# and a 2D T# was accepted"); +} + void verifyUnusedUnnormalizedSampler() { using namespace ShaderRecompiler; ImageResource image{}; @@ -5955,6 +5996,7 @@ int main(int argc, char** argv) { verifyCoherentBufferAccesses(); verifyUnnormalizedSamplers(); verifyUnusedUnnormalizedSampler(); + verifyNullVolumeUnnormalizedSampler(); verifyWaveUniformValues(); verifyTwoLaneUniformValues(); verifyBdaReadFallbackFunctions(); diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 2a33e8a6d8..24b61dcff4 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -56,6 +56,7 @@ public: static std::uint32_t RuntimeImageMode(const ImageResource& image, const DescriptorValue& descriptor, std::span modes); static void PrepareImageModes(ShaderInfo& info); static const char* UnnormalizedSampleMismatch(const ImageResource& image); + static bool NullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor); static ResolvedImageTables ResolveImageTables(const ShaderInfo& info, const ResourceSnapshot& snapshot, std::uint64_t memoKey = 0); [[nodiscard]] IrResourcePlan ExtractPlan(const IrProgram& program) const; static void CompactPlan(IrResourcePlan& plan); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 79e73779e2..e4544a55d9 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -182,10 +182,11 @@ struct UnnormalizedProof { std::vector samplers; std::vector images; std::vector imagesAtBaseLevel; + std::vector samplersIgnoreCoordinates; }; UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapshot& snapshot) { - UnnormalizedProof proof{std::vector(info.samplers.size()), std::vector(info.images.size()), std::vector(info.images.size())}; + UnnormalizedProof proof{std::vector(info.samplers.size()), std::vector(info.images.size()), std::vector(info.images.size()), std::vector(info.samplers.size())}; for (std::uint32_t r = 0; r < info.samplers.size(); r++) { if (snapshot.samplers.at(r).dwordCount != 4u) fail("sampler descriptor must contain four dwords"); const auto& sampler = info.samplers[r]; @@ -212,6 +213,8 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh if (sampler.depthCompare) { failUnnormalized("is used with depth comparison"); } + bool sampled = false; + const char* nullMismatch = nullptr; for (const auto& pair : info.sampledPairs) { if (pair.sampler != r) { continue; @@ -221,12 +224,23 @@ UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapsh failUnnormalized("samples an image selected at run time"); } const auto& image = info.runtimeImageModes.at(pair.image).at(ResourceMaterializer::RuntimeImageMode(base, snapshot.images.at(pair.image), info.runtimeImageModes.at(pair.image))); - if (const auto* reason = ResourceMaterializer::UnnormalizedSampleMismatch(image)) { + const auto* reason = ResourceMaterializer::UnnormalizedSampleMismatch(image); + if (reason != nullptr && ResourceMaterializer::NullImageDescriptor(base, snapshot.images.at(pair.image))) { + if (nullMismatch == nullptr) nullMismatch = reason; + continue; + } + if (reason != nullptr) { failUnnormalized(reason); } if (image.constantSwizzle) continue; + sampled = true; proof.images[pair.image] = true; } + if (!sampled && nullMismatch != nullptr) { + proof.samplersIgnoreCoordinates[r] = true; + continue; + } + if (nullMismatch != nullptr) failUnnormalized(nullMismatch); proof.samplers[r] = true; } for (std::uint32_t resource = 0; resource < info.images.size(); resource++) { @@ -451,6 +465,7 @@ DescriptorBindingPlan DescriptorBindingBuilder::Prepare(const IrBindingLayout& l for (const auto& pair : info.sampledPairs) if (pair.sampler == resource) compare |= info.images.at(pair.image).depthCompare && compareStates.at(pair.image) == 0u; physical.samplerDepthCompare.push_back(compare); physical.samplerUnnormalized.push_back(unnormalized.samplers.at(resource)); + physical.samplerCoordinatesIgnored.push_back(unnormalized.samplersIgnoreCoordinates.at(resource)); const auto mode = 1u << (originals[element] & 1u); physical.samplerIntegerBorder.push_back((samplerIntegerModes.at(resource) & mode) != 0u && (samplerFloatModes.at(resource) & mode) == 0u); const auto& words = snapshot.samplers.at(resource).dwords; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index ca0eef25c8..abb8a15e5e 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -1623,6 +1623,10 @@ std::vector ResourceMaterializer::RuntimeImageModes(const ImageRe return modes; } +bool ResourceMaterializer::NullImageDescriptor(const ImageResource& image, const DescriptorValue& descriptor) { + return nullImageDescriptor(image, descriptor); +} + const char* ResourceMaterializer::UnnormalizedSampleMismatch(const ImageResource& image) { if (image.table != NoTable) return "samples an image selected at run time"; if (image.constantSwizzle) return nullptr; diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 38de551273..9f9133417d 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -428,6 +428,7 @@ struct DescriptorBinding { std::vector bufferWritten; std::vector samplerUnnormalized; std::vector samplerIntegerBorder; + std::vector samplerCoordinatesIgnored; std::vector imageUnnormalized; std::vector imageUnnormalizedBaseLevel; std::vector imageSamplers; diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 3c9e663e94..d4925d237e 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -427,6 +427,7 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Flags(binding.imageUnnormalized); writer.Flags(binding.imageUnnormalizedBaseLevel); writer.Flags(binding.samplerIntegerBorder); + writer.Flags(binding.samplerCoordinatesIgnored); writer.Values(std::span(binding.imageSamplers)); writer.Flags(binding.bufferRead); } @@ -454,6 +455,7 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) { reader.Flags(binding.imageUnnormalized); reader.Flags(binding.imageUnnormalizedBaseLevel); reader.Flags(binding.samplerIntegerBorder); + reader.Flags(binding.samplerCoordinatesIgnored); reader.Values(binding.imageSamplers); reader.Flags(binding.bufferRead); } diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index f9840a0963..bcb99a770a 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 = 37; +inline constexpr std::uint32_t FormatVersion = 38; enum class LoadStatus { Loaded, From ca7f2f60b5abb3ee4cf9ab4c3dc31bc916e9575b Mon Sep 17 00:00:00 2001 From: Gjon Gojcevic Date: Sat, 17 Oct 2026 18:49:34 -0400 Subject: [PATCH 105/131] fix(shader): update the DescriptorBinding size assert for samplerCoordinatesIgnored The new samplerCoordinatesIgnored field adds 40 bytes to DescriptorBinding under libstdc++ on Linux x86_64, where the size assert is compiled. The encoder and decoder already carry the field. AI-assisted: yes --- core/shader/recompiler/ShaderDiskCache.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index d4925d237e..6724f0458d 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -87,7 +87,7 @@ namespace ShaderRecompiler::ShaderDiskCache { static_assert(sizeof(CompiledShaderArtifact) == 240, "CompiledShaderArtifact changed: update the artifact encoder"); static_assert(sizeof(ShaderInvocation) == 200, "ShaderInvocation changed: update the invocation encoder"); static_assert(sizeof(RecompileResult) == 472, "RecompileResult changed: update EncodeResult and DecodeResult"); -static_assert(sizeof(DescriptorBinding) == 592, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(DescriptorBinding) == 632, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 32, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(VertexInput) == 16, "VertexInput changed: update the vertex input encoder"); static_assert(sizeof(FragmentParameter) == 16, "FragmentParameter changed: update the parameter encoder"); From d10c1b43ba938bd9a4d22243d22057b47491543e Mon Sep 17 00:00:00 2001 From: mugurc Date: Sat, 17 Oct 2026 18:01:16 +0300 Subject: [PATCH 106/131] fix(libc): keep the app0.zar state through exit handlers Every path resolution reaches the app0.zar state, which was a function-local static: it is destroyed during exit before the handlers registered ahead of its first use run, so a path resolved in such a handler locked a destroyed mutex. glibc lets that pass; macOS fails it with EINVAL, and guest_path_lifetime failed there. The state is now allocated once and never destroyed, as the working directory state beside it already is. --- core/libs/prx/libc/src/AppArchive.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/AppArchive.cpp b/core/libs/prx/libc/src/AppArchive.cpp index be3916a765..ba32feae07 100644 --- a/core/libs/prx/libc/src/AppArchive.cpp +++ b/core/libs/prx/libc/src/AppArchive.cpp @@ -22,8 +22,8 @@ struct Archive { }; Archive& State() { - static Archive archive; - return archive; + static auto* archive = new Archive; + return *archive; } bool SameComponent(std::string_view left, std::string_view right) { From 2d8d3b9dc15f28c6b1a7494e1b969fca9ff59003 Mon Sep 17 00:00:00 2001 From: babyhuey <7751607+babyhuey@users.noreply.github.com> Date: Wed, 14 Oct 2026 01:20:41 -0400 Subject: [PATCH 107/131] fix(libc): keep the host standard stream when a guest freopen of it fails --- core/libs/prx/libc/CMakeLists.txt | 3 +- core/libs/prx/libc/include/FileStream.hpp | 35 +++++++++++++++++++++++ core/libs/prx/libc/include/General.hpp | 1 + core/libs/prx/libc/src/FileAndHeap.cpp | 3 +- core/libs/prx/libc/src/General.cpp | 10 +++++++ core/libs/tests/GuestStandardStreams.cpp | 14 +++++++++ 6 files changed, 64 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 7344519b44..23f2780f7f 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -463,8 +463,9 @@ if(BUILD_TESTING) configure_windows_unwind(guest_standard_stream_tests) add_test(NAME guest_standard_streams COMMAND guest_standard_stream_tests) add_test(NAME guest_standard_streams_closed_stdout COMMAND guest_standard_stream_tests --closed-stdout) + add_test(NAME guest_standard_streams_exclusive_stdout COMMAND guest_standard_stream_tests --exclusive-stdout) if(WIN32) - set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_standard_streams guest_standard_streams_closed_stdout guest_standard_streams_exclusive_stdout PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(file_streams_tests ${LIBS_INCLUDE_DIR}/tests/FileStreams.cpp) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index 0f51bc7d6d..a72d143805 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_FILESTREAM_HPP +#include #include #include #include @@ -15,7 +16,10 @@ #include "prx/libc/include/FileLock.hpp" extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle); +#else +#include #endif +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor); struct GuestFilePrefix { unsigned char* position = nullptr; @@ -80,6 +84,31 @@ public: } GuestFilePrefix& GuestState() { return _guest; } + bool IsHostStandard() const { + return _handle == stdin || _handle == stdout || _handle == stderr; + } + + void Detach() { + _handle = nullptr; + _guest.flags = 0; + _guest.descriptor = -1; + } + + void RestoreHostStandard(std::FILE* stream, int kept) { + const int error = errno; +#ifdef _WIN32 + const bool restored = _handle || (::_wfreopen(L"NUL", stream == stdin ? L"r" : L"w", stream) && ::_dup2(kept, ::_fileno(stream)) == 0); + ::_unlock_file(stream); + ::_close(kept); +#else + const bool restored = _handle || (std::freopen("/dev/null", stream == stdin ? "r" : "w", stream) && ::dup2(kept, ::fileno(stream)) >= 0); + ::funlockfile(stream); + ::close(kept); +#endif + if (!restored) throw std::runtime_error("FileStream: cannot restore the host standard stream"); + errno = error; + } + bool Reopen(const std::filesystem::path& filename, const char* mode) { auto* previous = GetHandle(); _guest = {}; @@ -90,11 +119,17 @@ public: const int replaced = previous ? _fileno(previous) : -1; auto* const standard = HostStandardHandle_nid_no_patch(replaced); GuestFileLockForget_nid_no_patch(replaced); + const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(replaced) : -1; + if (kept >= 0) ::_lock_file(previous); _handle = ::_wfreopen(filename.c_str(), wideMode.c_str(), previous); + if (kept >= 0) RestoreHostStandard(previous, kept); KeepStandardHandle_nid_no_patch(replaced, standard); #else if (::fileno(previous) == 2) KeepHostStandardError_nid_no_patch(); + const int kept = IsHostStandard() ? DuplicateHostDescriptor_nid_no_patch(::fileno(previous)) : -1; + if (kept >= 0) ::flockfile(previous); _handle = std::freopen(filename.c_str(), mode, previous); + if (kept >= 0) RestoreHostStandard(previous, kept); #endif if (!_handle) return false; _guest.flags = 0x10; diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 34cee2743f..c60a1704fa 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -35,6 +35,7 @@ extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path extern "C" std::vector WrittenPaths_nid_no_patch(); extern "C" void SyncWrittenPaths_nid_no_patch(); extern "C" void BlockPathAlias_nid_no_patch(const char* guestPrefix); +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor); #ifdef _WIN32 extern "C" int WindowsOpenError_nid_no_patch(unsigned long error); extern "C" void* HostStandardHandle_nid_no_patch(int descriptor); diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index fa8e028a98..bd221c5d2b 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -283,7 +283,8 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode std::filesystem::path path; if (*filename) { if (const int error = ResolvePathError_nid_no_patch(filename, path)) { - stream->Close(); + if (stream->IsHostStandard()) stream->Detach(); + else stream->Close(); if (stream->IsDynamic()) delete stream; errno = error; return nullptr; diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index d96670de58..75c5e8b50c 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -356,6 +356,10 @@ extern "C" int KeepHostStandardError_nid_no_patch() { extern "C" int HostStandardError_nid_no_patch() { return g_hostStandardError.load(); } + +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { + return ::fcntl(descriptor, F_DUPFD_CLOEXEC, 3); +} #endif extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath) { @@ -612,6 +616,12 @@ extern "C" void KeepStandardHandle_nid_no_patch(int descriptor, void* handle) { if (handle != nullptr && !SetStdHandle(StandardHandleId(descriptor), handle)) throw std::runtime_error("Cannot keep the host standard handle of descriptor " + std::to_string(descriptor)); } +extern "C" int DuplicateHostDescriptor_nid_no_patch(int descriptor) { + const auto previous = _set_thread_local_invalid_parameter_handler([](const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {}); + const int kept = ::_dup(descriptor); + _set_thread_local_invalid_parameter_handler(previous); + return kept; +} #endif extern "C" [[noreturn]] void NotImplemented_nid_no_patch(const char* funcName) { diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index dca63ca158..0ba21b86b1 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -566,11 +566,25 @@ static int CheckClosedStandardOutput() { Require(puts_nid_postfix("text") == -1 && *__error_nid_postfix() == 9); Require(PrintStandard("%d", 1) == -1 && *__error_nid_postfix() == 9); Require(fputs_nid_postfix("text", __stdoutp_nid_postfix) == -1 && *__error_nid_postfix() == 9); + Require(std::fputs("host output\n", stdout) >= 0 && std::fflush(stdout) == 0 && !std::ferror(stdout)); + Require(freopen_nid_postfix("missing-closed-stream-dir/err.log", "a", __stderrp_nid_postfix) == nullptr); + Require(std::fputs("host error\n", stderr) >= 0 && std::fflush(stderr) == 0 && !std::ferror(stderr)); + return 0; +} + +static int CheckExclusiveStandardOutput() { + const auto filename = "anyps5-exclusive-stdout-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + Require(freopen_nid_postfix(filename.c_str(), "wx", __stdoutp_nid_postfix) == __stdoutp_nid_postfix); + Require(fputs_nid_postfix("exclusive\n", __stdoutp_nid_postfix) >= 0 && fflush_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(fclose_nid_postfix(__stdoutp_nid_postfix) == 0); + Require(CheckFileBytes(filename, "exclusive\n", "wx", true)); + Require(std::filesystem::remove(filename)); return 0; } int main(int argc, char** argv) { if (argc > 1 && std::string_view(argv[1]) == "--closed-stdout") return CheckClosedStandardOutput(); + if (argc > 1 && std::string_view(argv[1]) == "--exclusive-stdout") return CheckExclusiveStandardOutput(); CheckClosedStreamWrites(); CheckDescriptorAccessModes(); Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); From 536fef3228c81ebe7596e3eb184af64967e496da Mon Sep 17 00:00:00 2001 From: Slick Daddy <129640104+slick-daddy@users.noreply.github.com> Date: Sun, 18 Oct 2026 16:57:56 +0300 Subject: [PATCH 108/131] fix(libc): format %p the way the console's libc does The console's libSceLibcInternal.sprx (measured on firmware 12.70 in #7549) prints %p as bare lowercase hex without a 0x prefix, prints 0 for a null pointer, ignores #, + and the space flag, counts only the digits towards the width, pads with spaces on the left (on the right for - and with zeros for 0 when there is no precision), treats the precision as the minimum number of digits, prints nothing for a null pointer under %.0p, and accepts the l length. FormatPointer instead followed FreeBSD and printed 0x..., which is what a title does not see on hardware. FormatPointer now prints the digits alone, the narrow Windows formatter accepts %lp, and Linux routes %p and %lp to the guest formatter instead of glibc, which prints the FreeBSD form and (nil) for a null pointer. The wide family still refuses %lp, because the console's swprintf was not measured with it; it returns -1 as it does for any other conversion the formatter rejects. The debt note records the pointer cases the console was not measured with and what the Linux routing means for a format that mixes %p with a refused conversion. --- .../prx/libc/include/WindowsFormatting.hpp | 11 +++--- core/libs/prx/libc/src/Formatting.cpp | 37 ++++++++++--------- core/libs/tests/Formatting.cpp | 29 +++++++++------ core/libs/tests/GuestWideFormatting.cpp | 11 ++++-- docs/dev/debt/libc.md | 3 +- 5 files changed, 50 insertions(+), 41 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 436549ba2c..ff0fd39cfa 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -193,11 +193,10 @@ inline std::string FormatPointer(const std::string& spec, const void* pointer) { for (unsigned long long rest = value; rest != 0; rest >>= 4) digits.insert(digits.begin(), "0123456789abcdef"[rest & 15]); if (digits.empty() && !(hasPrecision && precision == 0)) digits = "0"; if (digits.size() < precision) digits.insert(0, precision - digits.size(), '0'); - const std::size_t used = digits.size() + 2; - const std::size_t padding = width > used ? width - used : 0; - if (left) return "0x" + digits + std::string(padding, ' '); - if (zero && !hasPrecision) return "0x" + std::string(padding, '0') + digits; - return std::string(padding, ' ') + "0x" + digits; + const std::size_t padding = width > digits.size() ? width - digits.size() : 0; + if (left) return digits + std::string(padding, ' '); + if (zero && !hasPrecision) return std::string(padding, '0') + digits; + return std::string(padding, ' ') + digits; } constexpr size_t maximumArgumentPosition = 99; @@ -380,7 +379,7 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo } } output.Value(spec + 's', utf8.c_str()); - } else if (conversion == 'p' && length.empty()) { + } else if (conversion == 'p' && (length.empty() || length == "l")) { const std::string text = FormatPointer(spec, args.Next()); output.Append(text.data(), text.size()); } else if (conversion == 'm') { diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 7a77dad2de..bb4163aa43 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -31,13 +31,14 @@ int __isoc99_vfscanf(std::FILE*, const char*, std::va_list); namespace { #ifndef _WIN32 -bool HasGuestWideFormat(const char* format) { +bool RequiresGuestFormatting(const char* format) { if (format == nullptr) return false; while ((format = std::strchr(format, '%')) != nullptr) { ++format; if (*format == '%') { ++format; continue; } while (*format && std::strchr("-+ #0'0123456789.*$", *format)) ++format; - if (*format == 'S' || *format == 'C' || *format == 'm' || (*format == 'l' && (format[1] == 's' || format[1] == 'c'))) return true; + if (*format == 'S' || *format == 'C' || *format == 'm' || *format == 'p' || + (*format == 'l' && (format[1] == 's' || format[1] == 'c' || format[1] == 'p'))) return true; if (*format) ++format; } return false; @@ -222,15 +223,15 @@ int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaLi const char* source = text.c_str(); #else std::string guestText; - const bool guestWide = HasGuestWideFormat(format); + const bool guestFormatting = RequiresGuestFormatting(format); char* text = nullptr; std::va_list copy; va_copy(copy, *reinterpret_cast(args)); - const int count = guestWide ? LibcDetail::FormatWindows(nullptr, 0, format, args, &guestText) : ::vasprintf(&text, format, copy); + const int count = guestFormatting ? LibcDetail::FormatWindows(nullptr, 0, format, args, &guestText) : ::vasprintf(&text, format, copy); va_end(copy); std::unique_ptr owner(text, std::free); if (count < 0) return -1; - const char* source = guestWide ? guestText.c_str() : text; + const char* source = guestFormatting ? guestText.c_str() : text; #endif auto* output = static_cast(ApplicationHeapAllocate_nid_no_patch(static_cast(count) + 1)); if (!output) { @@ -255,7 +256,7 @@ int APS5_VABI vfprintf_nid_postfix(FileStream* stream, const char* format, VaLis const int result = std::fwrite(buffer.data(), 1, static_cast(count), native) == static_cast(count) ? count : -1; #else - if (HasGuestWideFormat(format)) { + if (RequiresGuestFormatting(format)) { std::string buffer; const int count = LibcDetail::FormatWindows(nullptr, 0, format, args, &buffer); const int result = std::fwrite(buffer.data(), 1, static_cast(count), native) == static_cast(count) ? count : -1; @@ -349,7 +350,7 @@ int APS5_VABI printf_nid_postfix(const char* format, ...) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); va_end(args); return result; } @@ -358,7 +359,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { if (!_Stdout_nid_postfix.IsOpen()) { errno = 9; return -1; } std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); va_end(args); return result; } @@ -366,7 +367,7 @@ int APS5_VABI libc_printf_nid_postfix(const char* format, ...) { int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -374,7 +375,7 @@ int APS5_VABI snprintf_nid_postfix(char* buffer, size_t size, const char* format int APS5_VABI sprintf_nid_postfix(char* buffer, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, SIZE_MAX, format, args) : std::vsprintf(buffer, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, SIZE_MAX, format, args) : std::vsprintf(buffer, format, args); va_end(args); return result; } @@ -441,7 +442,7 @@ int APS5_VABI vprintf_nid_postfix(const char* str, VaList* c) { #ifdef _WIN32 return LibcDetail::PrintWindows(str, c); #else - if (HasGuestWideFormat(str)) return LibcDetail::PrintWindows(str, c); + if (RequiresGuestFormatting(str)) return LibcDetail::PrintWindows(str, c); std::va_list copy; va_copy(copy, *reinterpret_cast(c)); const int result = std::vprintf(str, copy); @@ -454,7 +455,7 @@ int APS5_VABI vsprintf_nid_postfix(char* str, const char* format, VaList* args) #ifdef _WIN32 return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); #else - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(str, SIZE_MAX, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsprintf(str, format, copy); @@ -467,7 +468,7 @@ int APS5_VABI vsnprintf_nid_postfix(char* str, size_t size, const char* format, #ifdef _WIN32 return LibcDetail::FormatWindows(str, size, format, c); #else - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(str, size, format, c); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(str, size, format, c); std::va_list copy; va_copy(copy, *reinterpret_cast(c)); const int result = std::vsnprintf(str, size, format, copy); @@ -512,7 +513,7 @@ int APS5_VABI vsscanf_s_nid_postfix(const char* buffer, const char* format, VaLi } int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) { - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(buffer, size, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(buffer, size, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsnprintf(buffer, size, format, copy); @@ -521,7 +522,7 @@ int APS5_VABI vsnprintf_s_nid_postfix(char* buffer, size_t size, const char* for } int APS5_VABI vsprintf_s_nid_postfix(char* buffer, size_t size, const char* format, VaList* args) { - if (HasGuestWideFormat(format)) return LibcDetail::FormatWindows(buffer, size, format, args); + if (RequiresGuestFormatting(format)) return LibcDetail::FormatWindows(buffer, size, format, args); std::va_list copy; va_copy(copy, *reinterpret_cast(args)); const int result = std::vsnprintf(buffer, size, format, copy); @@ -532,7 +533,7 @@ int APS5_VABI vsprintf_s_nid_postfix(char* buffer, size_t size, const char* form int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -540,7 +541,7 @@ int APS5_VABI sprintf_s_nid_postfix(char* buffer, size_t size, const char* forma int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::FormatWindows(buffer, size, format, args) : std::vsnprintf(buffer, size, format, args); va_end(args); return result; } @@ -548,7 +549,7 @@ int APS5_VABI snprintf_s_nid_postfix(char* buffer, size_t size, const char* form int APS5_VABI printf_s_nid_postfix(const char* format, ...) { std::va_list args; va_start(args, format); - const int result = HasGuestWideFormat(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); + const int result = RequiresGuestFormatting(format) ? LibcDetail::PrintWindows(format, args) : std::vprintf(format, args); va_end(args); return result; } diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index e8693b984e..2db861b0f5 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -177,23 +177,28 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { Require(threw); } char expected[1024]; - std::snprintf(expected, sizeof(expected), "%#08x %.3e %a %g 0x%llx", 42u, 1.25, 1.25, 1.25, + std::snprintf(expected, sizeof(expected), "%#08x %.3e %a %g %llx", 42u, 1.25, 1.25, 1.25, static_cast(reinterpret_cast(buffer))); snprintf_nid_postfix(buffer, sizeof(buffer), "%#08x %.3e %a %g %p", 42u, 1.25, 1.25, 1.25, static_cast(buffer)); Require(std::strcmp(buffer, expected) == 0); -#ifdef _WIN32 char pointers[128]; const auto address = [](std::uintptr_t value) { return reinterpret_cast(value); }; - Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 23 && - std::strcmp(pointers, "0x0|0x1234|0xdeadbeef12") == 0); - Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%8p][%-8p][%08p][%.6p][%10.4p][%.0p]", - address(0x1a), address(0x1a), address(0x1a), address(0x1a), address(0x1a), address(0)) == 56 && - std::strcmp(pointers, "[ 0x1a][0x1a ][0x00001a][0x00001a][ 0x001a][0x]") == 0); - Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%+p][% p][%#p][%*p][%-*p]", address(0x1a), address(0x1a), - address(0x1a), 6, address(0xf), -6, address(0xf)) == 34 && - std::strcmp(pointers, "[0x1a][0x1a][0x1a][ 0xf][0xf ]") == 0); - Require(snprintf_nid_postfix(pointers, 6, "%p", address(0xabcdef)) == 8 && std::strcmp(pointers, "0xabc") == 0); -#endif + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 17 && + std::strcmp(pointers, "0|1234|deadbeef12") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%p][%5p][%-5p][%05p][%.8p][%10.6p][%.0p]", + address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0)) == 51 && + std::strcmp(pointers, "[1234][ 1234][1234 ][01234][00001234][ 001234][]") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%#p][%+p][% p][%lp][%20p][%-20p]", + address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234), address(0x1234)) == 68 && + std::strcmp(pointers, "[1234][1234][1234][1234][ 1234][1234 ]") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%08p][%.6p][%8p]", address(0x1a), address(0x1a), address(0xf)) == 28 && + std::strcmp(pointers, "[0000001a][00001a][ f]") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "[%*p][%-*p]", 6, address(0x1a), -6, address(0x1a)) == 16 && + std::strcmp(pointers, "[ 1a][1a ]") == 0); + Require(FormatList(pointers, sizeof(pointers), "%2$p:%1$d", 7, address(0)) == 3 && std::strcmp(pointers, "0:7") == 0); + Require(snprintf_nid_postfix(pointers, 6, "%p", address(0xabcdef)) == 6 && std::strcmp(pointers, "abcde") == 0); + Require(snprintf_nid_postfix(pointers, sizeof(pointers), "%p", address(0xffffffffffffffffull)) == 16 && + std::strcmp(pointers, "ffffffffffffffff") == 0); snprintf_nid_postfix(buffer, sizeof(buffer), "%.*s", -1, "unlimited"); Require(std::strcmp(buffer, "unlimited") == 0); buffer[5] = '!'; diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index f372fd47e1..818f401ffd 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -114,10 +114,13 @@ static void CheckPositional() { static void CheckPointer() { char16_t buffer[64]; const auto address = [](std::uintptr_t value) { return reinterpret_cast(value); }; - Require(Format(buffer, 64, u"%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 23 && - buffer == std::u16string(u"0x0|0x1234|0xdeadbeef12"), "vswprintf %p uses the 0x form"); - Require(Format(buffer, 64, u"[%8p][%-8p][%08p][%.6p]", address(0x1a), address(0x1a), address(0x1a), address(0x1a)) == 40 && - buffer == std::u16string(u"[ 0x1a][0x1a ][0x00001a][0x00001a]"), "vswprintf %p width and precision"); + Require(Format(buffer, 64, u"%p|%p|%p", address(0), address(0x1234), address(0xdeadbeef12)) == 17 && + buffer == std::u16string(u"0|1234|deadbeef12"), "vswprintf %p uses the console form"); + Require(Format(buffer, 64, u"[%10p][%-10p][%.8p][%08p]", address(0x1234), address(0x1234), address(0x1234), address(0x1a)) == 44 && + buffer == std::u16string(u"[ 1234][1234 ][00001234][0000001a]"), "vswprintf %p width and precision"); + Require(Format(buffer, 64, u"[%.0p][%p]", address(0), address(0)) == 5 && + buffer == std::u16string(u"[][0]"), "vswprintf %p with a zero precision prints nothing"); + Require(Format(buffer, 64, u"[%lp]", address(0x1234)) == -1, "vswprintf %lp is refused"); } static void CheckCount() { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index d3ac2a4d01..e665a10863 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -77,7 +77,8 @@ - [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 -- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments or `%m`, so a narrow `%p` and a NaN there still follow the host (glibc prints a null pointer as `(nil)`, where FreeBSD prints `0x0`, and a NaN with its sign, `-nan`, and with the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter +- [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) converts 16-bit guest wide arguments to UTF-8 on both platforms, independently of the host locale. It does not use the guest's single-byte `wcrtomb`; the console's treatment of invalid UTF-16 and locale-specific encoding remains unverified. Linux retains host formatting for formats without wide arguments, `%m` or `%p`, so a NaN there still follows the host (glibc prints its sign, `-nan`, and honors the `+` and space flags, where FreeBSD prints `nan`); argument positions above 99 (FreeBSD's `NL_ARGMAX`) throw when a format requires the guest formatter +- [%p and %lp](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) follow the console's `libSceLibcInternal.sprx` measured from a homebrew payload on firmware 12.70 ([#7549](https://github.com/boykopovar/AnyPS5/issues/7549)), not FreeBSD: bare lowercase hex with no `0x` prefix even under `#`, `0` for a null pointer, `+` and the space flag ignored, the width counting only the digits, `-` padding on the right, `0` padding with zeros when there is no precision, the precision as the minimum number of digits, nothing for a null pointer under `%.0p`, and `l` accepted. Only `snprintf` and `swprintf` of that module were measured, from a payload process rather than a title process, so a width beside `%.0p`, the `0` flag beside a precision (which C17 ignores) and `%.0p` of a non-null pointer follow this formatter's own rules and are not measured; `swprintf` was not run with `%lp`, so the wide family still refuses that conversion and returns -1. Linux routes `%p` and `%lp` to this formatter because glibc prints the FreeBSD form and `(nil)` for a null pointer, which means a format that combines `%p` with a conversion this formatter refuses, such as an argument position above 99, throws there instead of reaching glibc - [collate and collate _Getcat](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) build the facet for the C locale only and throw for any other locale name, the wide entry point following the narrow one it sits next to. The wide facet carries its own vtable, whose `compare`, `transform` and `hash` take the guest's 16-bit characters, so the length of a range is a count of characters: `compare` orders by 16-bit value (the C locale, as Dinkumware's `char_traits::compare` does), `transform` copies the characters into the result and records size and capacity in 16-bit elements (inline capacity 7, heap capacity `length | 7`, `(capacity + 1) * 2` bytes including the terminator), following Dinkumware's [VC12 `basic_string`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/xstring#L500-L531), and `hash` mixes one whole character per round, so wide text below 0x100 hashes like its narrow spelling and a character above 0xff hashes as one value instead of the two bytes it holds. The console's `libc.prx` exports both `_Getcat` (NIDs BSVJqITGCyI and 2wz4rthdiy8) with identical 0x19d-byte bodies, believed to build the same 0x20-byte facet; the console bodies themselves are unreadable (obfuscated dynsym, and the text of the relinked `libc.prx` is zeroed), so the wide entries follow the narrow algorithm and the C locale instead of the console's wide code, and no console run checks the two. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size. The narrow result capacity (`length | 15`) and the over-aligned storage of results of 4080 characters or more follow the console's `libc.prx` code and are not checked on hardware. No module of the PPSA59322, PPSA76677 or PPSA97358 dumps in this tree imports either `_Getcat`, so neither has been exercised by a title. - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, on Linux throws for the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`): glibc would store a 32-bit `wchar_t` per character into the guest's 16-bit buffer. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [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` From 267cb56a0de35bf4050f14214494843740040e73 Mon Sep 17 00:00:00 2001 From: Karim Ibrahim <40270714+karimibrahim07@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:46:36 -0400 Subject: [PATCH 109/131] fix(libkernel): report the precommitted stack of an adopted thread on Windows An adopted thread (the main thread, or a host thread that calls scePthreadSelf) reported its whole Windows stack reservation, and so did sceKernelIsStack on a host thread before adoption. A relinked executable reserves 0x210000 bytes for its main thread and commits the top 0x200000, so the lowest 64 KiB of the reported stack hold only reserved pages and Windows' guard pages. mprotect fails there, so HotSpot in Java 25 LAN / DNS (PPSA99869) could not create the main thread's stack guard pages ("Attempt to allocate stack guard pages failed"). When everything above the bottom allocation granule is committed read-write, the host stack is now reported from there, as created guest threads report only their committed stack. Other host stacks keep the whole range. The committed-pages walk is shared with StartNativeThread and throws when VirtualQuery fails. --- core/libs/prx/libkernel/CMakeLists.txt | 4 + .../libs/prx/libkernel/Pthread/src/Thread.cpp | 30 +++++-- core/libs/tests/GuestThreadSelf.cpp | 79 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 4 files changed, 107 insertions(+), 7 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index f3627ea391..18593eada7 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -420,6 +420,10 @@ if(BUILD_TESTING) add_test_executable(guest_thread_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadSelf.cpp) target_include_directories(guest_thread_self_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_thread_self_tests PRIVATE libkernel libc) + if(WIN32) + target_compile_definitions(guest_thread_self_tests PRIVATE _WIN32_WINNT=0x0602) + target_link_options(guest_thread_self_tests PRIVATE "SHELL:-Xlinker --stack=0x210000,0x200000") + endif() configure_windows_unwind(guest_thread_self_tests) add_test(NAME guest_thread_self COMMAND guest_thread_self_tests) set_tests_properties(guest_thread_self PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index fdbdb8a765..fe9aa84863 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -172,12 +172,32 @@ static void FreeGuestStack(PthreadPrivate*) {} static void RetireGuestStack(PthreadPrivate*) {} #endif +#ifdef _WIN32 +static bool CommittedReadWrite(std::uintptr_t begin, std::uintptr_t end) { + for (auto cursor = begin; cursor < end;) { + MEMORY_BASIC_INFORMATION memory{}; + if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory) || memory.RegionSize == 0) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "Querying the host thread stack"); + if (memory.State != MEM_COMMIT || memory.Protect != PAGE_READWRITE) return false; + cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + return true; +} + +static std::uintptr_t PrecommittedStackStart(std::uintptr_t low, std::uintptr_t high) { + SYSTEM_INFO system{}; + GetSystemInfo(&system); + const std::uintptr_t start = low + system.dwAllocationGranularity; + return start < high && CommittedReadWrite(start, high) ? start : low; +} +#endif + static bool HostStackLimits(std::uintptr_t* low, std::uintptr_t* high) { #ifdef _WIN32 ULONG_PTR lowLimit = 0; ULONG_PTR highLimit = 0; GetCurrentThreadStackLimits(&lowLimit, &highLimit); - *low = lowLimit; + *low = PrecommittedStackStart(lowLimit, highLimit); *high = highLimit; #else pthread_attr_t attr; @@ -674,12 +694,8 @@ static unsigned __stdcall StartNativeThread(void* opaque) { if (high <= low || high - low < self->stackSize) throw std::runtime_error("Cannot query guest thread stack"); self->stackAddress = reinterpret_cast(high - self->stackSize); - for (auto cursor = high - self->stackSize; cursor < high;) { - MEMORY_BASIC_INFORMATION memory{}; - if (VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) != sizeof(memory) || memory.State != MEM_COMMIT || memory.Protect != PAGE_READWRITE || memory.RegionSize == 0) - throw std::runtime_error("Guest thread stack is not fully committed"); - cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; - } + if (!CommittedReadWrite(high - self->stackSize, high)) + throw std::runtime_error("Guest thread stack is not fully committed"); self->threadId = std::this_thread::get_id(); CurrentGuestState().current = self; if (!self->name.empty()) { diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index c94f557187..87e38f6c0a 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -2,6 +2,12 @@ #include #include #include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif extern "C" { int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); @@ -31,6 +37,8 @@ int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_addr); int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size); +int APS5_VABI mprotect_nid_postfix(void* address, std::size_t length, int protection) noexcept; +int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end); } static bool StackContains(Pthread thread, const void* address) { @@ -58,6 +66,72 @@ static constexpr std::intptr_t WorkerRetval = 0x1234; static void Require(bool value) { if (!value) std::abort(); } +#ifdef _WIN32 +static constexpr std::uintptr_t GuestPage = 0x4000; +static constexpr int GuestProtNone = 0; +static constexpr int GuestProtReadWrite = 3; + +static void ReportedStack(Pthread thread, std::uintptr_t* begin, std::uintptr_t* end) { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK && scePthreadAttrGet(thread, &attr) == SCE_OK); + void* stack = nullptr; + size_t size = 0; + Require(scePthreadAttrGetstackaddr(&attr, &stack) == SCE_OK && scePthreadAttrGetstacksize(&attr, &size) == SCE_OK); + scePthreadAttrDestroy(&attr); + *begin = reinterpret_cast(stack); + *end = *begin + size; +} + +static void RequireGuardable(std::uintptr_t begin, std::uintptr_t end) { + Require(begin % GuestPage == 0 && end - begin >= GuestPage); + for (auto cursor = begin; cursor < end;) { + MEMORY_BASIC_INFORMATION memory{}; + Require(VirtualQuery(reinterpret_cast(cursor), &memory, sizeof(memory)) == sizeof(memory)); + Require(memory.State == MEM_COMMIT && memory.Protect == PAGE_READWRITE); + cursor = reinterpret_cast(memory.BaseAddress) + memory.RegionSize; + } + auto* lowest = reinterpret_cast(begin); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtReadWrite) == 0); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtNone) == 0); + Require(mprotect_nid_postfix(lowest, GuestPage, GuestProtReadWrite) == 0); +} + +static void RequireGuardableStack(Pthread thread) { + std::uintptr_t begin = 0; + std::uintptr_t end = 0; + ReportedStack(thread, &begin, &end); + RequireGuardable(begin, end); +} + +static DWORD WINAPI DefaultSizeHostThread(void*) { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + RequireGuardable(reinterpret_cast(start), reinterpret_cast(end)); + RequireGuardableStack(scePthreadSelf()); + return 0; +} + +static DWORD WINAPI PartlyCommittedHostThread(void*) { + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); + std::uintptr_t begin = 0; + std::uintptr_t end = 0; + ReportedStack(scePthreadSelf(), &begin, &end); + Require(begin == low && end == high); + return 0; +} + +static void RunHostThread(LPTHREAD_START_ROUTINE routine, SIZE_T committed) { + const HANDLE thread = CreateThread(nullptr, committed, routine, nullptr, 0, nullptr); + Require(thread != nullptr); + Require(WaitForSingleObject(thread, INFINITE) == WAIT_OBJECT_0); + Require(CloseHandle(thread) != 0); +} +#endif + static std::int64_t ThreadIdField(Pthread thread) { std::int64_t tid = 0; std::memcpy(&tid, static_cast(thread), sizeof(tid)); @@ -110,6 +184,11 @@ int main() { Require(pthread_getthreadid_np_nid_postfix() == static_cast(mainTid)); int local = 0; Require(StackContains(mainSelf, &local)); +#ifdef _WIN32 + RequireGuardableStack(mainSelf); + RunHostThread(DefaultSizeHostThread, 0); + RunHostThread(PartlyCommittedHostThread, 0x10000); +#endif Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EDEADLK); Require(pthread_join_nid_postfix(mainSelf, nullptr) == GUEST_EDEADLK); Require(scePthreadDetach(mainSelf) == SCE_KERNEL_ERROR_EINVAL); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index baef0291c7..c9f4961568 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -135,6 +135,7 @@ - [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 +- [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) builds the handler's `ucontext` with FreeBSD's amd64 layout (16-byte sigmask, `mcontext` at 0x40, `mc_rsp` at 0xf8); the console's layout is not verified beyond the fields PS5Util reads From 902b0515a2b8edfbb8ba9ccbcf2bac17916c6f6e Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 18:44:46 +0000 Subject: [PATCH 110/131] fix(agc): account recorded storage images in keep budget --- .../Execution/src/VulkanDevice.cpp | 9 +++++- .../Graphics/include/ShaderResources.hpp | 1 + .../Graphics/include/Texture.hpp | 1 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 ++++ .../Graphics/src/ShaderResources.cpp | 22 +++++++++++-- .../libSceAgcDriver/Graphics/src/Texture.cpp | 32 ++++++++++++------- 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index bdbb94ebe8..58e893e4cb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -4554,6 +4554,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) { const auto commands = recorder.Commands(&covered); recordStep(PhaseRecordCommands); recorder.Keep(record.objects); + if (record.resources != nullptr) record.resources->KeepStorageImageBytes(recorder); recorder.Keep(record.resources); if (record.resources != nullptr) recorder.KeepBytes(record.resources.get(), record.resources->CopiedBytes()); if (record.bindings != nullptr) { @@ -4861,7 +4862,10 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec static const bool deferEviction = std::getenv("APS5_NO_DEFERRED_EVICTION") == nullptr; std::vector> evicted; state->resourceCache.Insert(contentKey, resources, deferEviction ? &evicted : nullptr); - for (auto& object : evicted) state->recorder->Keep(std::move(object)); + for (auto& object : evicted) { + object->KeepStorageImageBytes(*state->recorder); + state->recorder->Keep(std::move(object)); + } if (profile) { timer.add(PhaseResourcesInsert, std::chrono::duration(std::chrono::steady_clock::now() - insertStart).count()); if (arguments != 0) d.indirectHold.phaseCounts[PhaseResourcesInsert] += evicted.size(); @@ -4959,6 +4963,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec state->resourceCache.Remove(contentKey, cached.get()); ++d.cacheInvalidated; // Off the mutex with its batch, as an evicted entry (insert()). + cached->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(cached)); } if (profile) { @@ -5137,6 +5142,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu // ordinary path runs with the prepared find (C10: the template goes to the batch). if (verify->cached.get() != hit->resources.get()) { counters.verifyReplaced.fetch_add(1, std::memory_order_relaxed); + hit->resources->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(hit->resources)); return RecipeOutcome::Rebuild; } @@ -5173,6 +5179,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu state->resourceCache.Remove(recipe.key, hit->resources.get()); ++d.cacheInvalidated; // Off the mutex with its batch, as an evicted entry. + hit->resources->KeepStorageImageBytes(*state->recorder); state->recorder->Keep(std::move(hit->resources)); counters.rebuilds[static_cast(report.failure)].fetch_add(1, std::memory_order_relaxed); return RecipeOutcome::Rebuild; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index a2a248e09f..9982792eb7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -245,6 +245,7 @@ public: // ranges are fixed by the build), so a Revalidate's collects on the same worker are memo hits. void PrecollectSurfaces() const; bool Reusable() const { return reusable; } + void KeepStorageImageBytes(Recorder& recorder) const; const std::vector& MultisampleSources() const { return multisampleSources; } static bool NeverReusable(std::span shaders); static std::uint64_t DepthGuardRefusals(); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 13dc4cadca..df86e88f1a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -359,6 +359,7 @@ public: static std::uint64_t RefreshesProved(); static std::uint64_t UntrackedCompares(); std::uint64_t GuestBytes() const; + std::uint64_t BudgetBytes() const; std::size_t OriginalSnapshotBytes() const { return original.capacity(); } VkDeviceSize AllocationBytes() const { return memoryBytes; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index 8ef1050892..e7d0b91ff9 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -257,6 +257,7 @@ void recordCmaskClear(const Context& context, Recorder& recorder, const HostImpo if (AnyShadowedOverlaps(address, bytes)) PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys); recorder.FlushKeyStoresOverlapping(address, bytes); const auto commands = recorder.Commands(); + if (resident != nullptr && !resident->Cached()) recorder.KeepBytes(resident.get(), static_cast(resident->BudgetBytes())); recorder.Keep(resident); context.detiler->DispatchCmaskClear(commands, import.buffer, HostImportOffset(import, address), bytes, view, color.extent.width, color.extent.height, color.elementBytes, color.clearWords, color.dccAddress == 0); recorder.NotePendingWrite(address, bytes); @@ -1544,6 +1545,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr kept->scratch = std::move(scratch); kept->targets = std::move(targets); const auto& residents = kept->targets; + resources->KeepStorageImageBytes(recorder); + for (const auto& target : residents) { + if (target != nullptr && !target->Cached()) recorder.KeepBytes(target.get(), static_cast(target->BudgetBytes())); + } recorder.Keep(kept); if (resources != nullptr) recorder.KeepBytes(resources.get(), resources->CopiedBytes()); if (checkRecords) recorder.OnComplete(std::move(checkRecords)); @@ -2678,6 +2683,7 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con if (!proved) { SharedResourceCache().Remove(recipe.key, resources.get()); countCache(&DrawProfile::cacheInvalidated); + resources->KeepStorageImageBytes(*recorder); recorder->Keep(std::move(resources)); timer.phase(PhaseLookup); return miss(DrawRecipeMiss::Proof); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index b8c914b8f4..19a7fb9545 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -432,7 +432,7 @@ void logLookup(const LookupRecord& record) { std::shared_ptr cachedStorageTexture(const Context& context, std::span words, const GuestTextureResource& resource, std::uint32_t mip, std::uint64_t guestBytes = 0, bool written = false); std::uint64_t heldBytes(const StorageTexture& image) { - return std::max(image.AllocationBytes(), image.GuestBytes()); + return image.BudgetBytes(); } bool MetadataMoved(const StorageTexture& image, const GuestTextureResource& resource) { @@ -1045,7 +1045,10 @@ std::shared_ptr cachedStorageTexture(const Context& context, std 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)); + for (auto& resource : resources) { + resource->KeepStorageImageBytes(*recorder); + recorder->Keep(std::move(resource)); + } } } if (!evicted.empty()) { @@ -1165,7 +1168,10 @@ void ClearCachedTextures(VkDevice device) { 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)); + for (auto& resource : resources) { + resource->KeepStorageImageBytes(*recorder); + recorder->Keep(std::move(resource)); + } } } } @@ -1342,6 +1348,16 @@ bool SameAsPreviousStorageElement(const ShaderRecompiler::DescriptorBinding& bin ShaderResources::ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes) : ShaderResources(context, std::array{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, static_cast(vertex.pushConstants.size())}}}, target, 1, indexAddress, indexBytes) {} +void ShaderResources::KeepStorageImageBytes(Recorder& recorder) const { + const auto keep = [&](const std::shared_ptr& image) { + if (image != nullptr && !image->Cached()) recorder.KeepBytes(image.get(), static_cast(image->BudgetBytes())); + }; + for (const auto& image : storageTextures) keep(image); + for (const auto& texture : textures) { + if (texture != nullptr) keep(texture->SharedStorageSource()); + } +} + ShaderResources::ShaderResources(const Context& context, std::span shaders, const ColorTarget& target, std::uint32_t colorAttachments, std::uint64_t indexAddress, std::size_t indexBytes, std::span snapshots) : context(context), guestMemory(context) { drawBuild = true; prepareAddressBindings(shaders, snapshots); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index 270d9ca0f8..9bc64c599a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -64,6 +64,12 @@ TextureProfile& Profile() { return profile; } +void keepStorageTexture(Recorder& recorder, std::shared_ptr texture) { + if (texture == nullptr) return; + if (!texture->Cached()) recorder.KeepBytes(texture.get(), static_cast(texture->BudgetBytes())); + recorder.Keep(std::move(texture)); +} + // APS5_PROFILE_DRAW, every 10 s on the [storage] line: storage image bytes uploaded by path // (recorded DCC clear, direct detile from the import, CPU) and written back by the reason of the // FlushPending that forced the write-back (the other callers name themselves: "refresh", "cache @@ -2305,7 +2311,7 @@ void StorageTexture::upload(const std::vector* layers) { if (recorder != nullptr) { commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::DccClear); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); ++Profile().storageRecordedClears; Recorder::CountBarriers(Recorder::CommandClass::DccClear, 2); if (Recorder::BarrierValidate()) { @@ -2397,7 +2403,7 @@ void StorageTexture::upload(const std::vector* layers) { timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload); if (linear) recorder->Keep(linear, linear->Size()); // The image itself must outlive the recorded copy: the cache may evict it right after. - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); // The detile reads the tiled bytes from the import when the batch runs. recorder->NotePendingRead(descriptor.baseAddress, static_cast(guestBytes), Recorder::ReadKind::StorageUpload); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3); @@ -2825,7 +2831,7 @@ std::uint64_t StorageTexture::uploadWindows(const HostImport& import, std::span< for (const auto& source : sources) { if (source.slab != nullptr) recorder->Keep(source.slab); } - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); if (!reads.empty()) recorder->NotePendingReads(reads, Recorder::ReadKind::StorageUpload); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, direct ? 2 : 3); if (Recorder::BarrierValidate()) { @@ -3029,7 +3035,7 @@ std::uint64_t StorageTexture::writeBackWindows(std::span se recorder->Keep(tiledScratch, tiledScratch->Size()); for (const auto& pieces : slabPieces) recorder->Keep(pieces.slab); for (const auto& slab : padding.slabs) recorder->Keep(slab); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, direct ? 3 : 4); if (Recorder::BarrierValidate()) { const std::pair read{image, false}; @@ -4103,7 +4109,7 @@ bool StorageTexture::clearByKeysFill(DccKeys keys, std::uint8_t key) { } const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::DccClear); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::DccClear, 2); if (Recorder::BarrierValidate()) { const std::pair clearedImage{image, true}; @@ -4238,7 +4244,7 @@ bool StorageTexture::FillClear(std::span pattern, std::u const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::FillClear); Recorder::CountBarriers(Recorder::CommandClass::FillClear, 2); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); if (Recorder::BarrierValidate()) { const std::pair cleared{image, true}; recorder->NoteAccess(Recorder::CommandClass::FillClear, Recorder::Access{{}, {}, std::span(&cleared, 1), VK_PIPELINE_STAGE_TRANSFER_BIT}); @@ -4365,8 +4371,8 @@ bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) { const auto commands = recorder->Commands(); const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy); Recorder::CountBarriers(Recorder::CommandClass::Copy, 2); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); - if (auto other = source.weak_from_this().lock()) recorder->Keep(std::move(other)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + if (auto other = source.weak_from_this().lock()) keepStorageTexture(*recorder, std::move(other)); if (Recorder::BarrierValidate()) { const std::array, 2> images{{{source.image, false}, {image, true}}}; recorder->NoteAccess(Recorder::CommandClass::Copy, Recorder::Access{{}, {}, images, VK_PIPELINE_STAGE_TRANSFER_BIT}); @@ -4543,8 +4549,8 @@ std::uint64_t StorageTexture::borrowUnits(StorageTexture& source, const std::vec if (recorder != nullptr) { commands = recorder->Commands(); timing = recorder->BeginGpuTiming(Recorder::CommandClass::StorageUpload); - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); - if (auto other = source.weak_from_this().lock()) recorder->Keep(std::move(other)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); + if (auto other = source.weak_from_this().lock()) keepStorageTexture(*recorder, std::move(other)); Recorder::CountBarriers(Recorder::CommandClass::StorageUpload, 2); if (Recorder::BarrierValidate()) { const std::array, 2> images{{{source.image, false}, {image, true}}}; @@ -4845,7 +4851,7 @@ void StorageTexture::writeBackLayers(const std::vector& layers) { recorder->Keep(tiledScratch, tiledScratch->Size()); for (const auto& slab : padding.slabs) recorder->Keep(slab); // The image itself must outlive the recorded retile: the cache may evict it right after. - if (auto self = weak_from_this().lock()) recorder->Keep(std::move(self)); + if (auto self = weak_from_this().lock()) keepStorageTexture(*recorder, std::move(self)); Recorder::CountBarriers(Recorder::CommandClass::StorageWriteBack, padding.copies.empty() ? 4 : 5); if (Recorder::BarrierValidate()) { const std::pair read{image, false}; @@ -5124,6 +5130,10 @@ std::uint64_t StorageTexture::GuestBytes() const { return guestBytes; } +std::uint64_t StorageTexture::BudgetBytes() const { + return std::max(AllocationBytes(), GuestBytes()); +} + VkImageView StorageTexture::View() const { return view; } From 27497f0474fc8f5e9df57ea031bf6b15b250ed00 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 19:45:43 +0000 Subject: [PATCH 111/131] fix(agc): account for batch-retained evicted storage images --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 6 ++ .../Graphics/src/ShaderResources.cpp | 1 + .../tests/execution/StorageCacheBudget.cpp | 62 ++++++++++++++++++- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fdca183551..2a0c0bf92d 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1958,6 +1958,12 @@ if(BUILD_TESTING) endif() endforeach() set_tests_properties(agc_driver_storage_cache_budget PROPERTIES TIMEOUT 30) + add_test(NAME agc_driver_storage_cache_retained_batch COMMAND agc_driver_storage_cache_budget_tests --retained-batch) + set_tests_properties(agc_driver_storage_cache_retained_batch PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + if(WIN32) + set_tests_properties(agc_driver_storage_cache_retained_batch PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory) set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index 19a7fb9545..8e3a12d880 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -1045,6 +1045,7 @@ std::shared_ptr cachedStorageTexture(const Context& context, std std::vector> resources; SharedResourceCache().RemoveImage(image.get(), &resources); if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) { + if (image.use_count() > 1) recorder->KeepBytes(image.get(), static_cast(heldBytes(*image))); for (auto& resource : resources) { resource->KeepStorageImageBytes(*recorder); recorder->Keep(std::move(resource)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp index 09c9c0b2da..a3a273ff50 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StorageCacheBudget.cpp @@ -1,4 +1,6 @@ +#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "Recompiler.hpp" @@ -10,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -17,6 +20,7 @@ namespace { using AgcDriver::Graphics::Require; +using AgcDriver::Graphics::Recorder; using AgcDriver::Graphics::StorageTexture; using ShaderRecompiler::ShaderStage; @@ -51,8 +55,7 @@ std::array TextureDescriptor(std::uint64_t address) { }; } -std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) { - const auto address = base + surface * Stride; +void Record(AgcDriver::VulkanDevice& device, std::uint64_t address) { std::vector userData(16, 0u); const auto texture = TextureDescriptor(address); std::copy(texture.begin(), texture.end(), userData.begin() + 4); @@ -67,7 +70,13 @@ std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint }; request.useCache = true; const auto result = ShaderRecompiler::Recompile(request); + std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex()); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); +} + +std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint64_t base, std::uint32_t surface, bool flush = true) { + const auto address = base + surface * Stride; + Record(device, address); device.WaitIdle(); if (!flush) return StorageTexture::FindLive(address, SurfaceBytes); StorageTexture::FlushPending(address, static_cast(SurfaceBytes), nullptr, "test"); @@ -75,10 +84,57 @@ std::shared_ptr Store(AgcDriver::VulkanDevice& device, std::uint return StorageTexture::FindLive(address, SurfaceBytes); } +int RetainedBatch() { +#ifdef _WIN32 + Require(_putenv_s("APS5_TEXTURE_CACHE_MIB", "32") == 0, "cannot set the test cache budget"); + Require(_putenv_s("APS5_NO_RESOURCE_CACHE", "1") == 0, "cannot disable the resource cache for the retained-batch test"); +#else + Require(setenv("APS5_TEXTURE_CACHE_MIB", "32", 1) == 0, "cannot set the test cache budget"); + Require(setenv("APS5_NO_RESOURCE_CACHE", "1", 1) == 0, "cannot disable the resource cache for the retained-batch test"); +#endif + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + std::vector storage((SurfaceBytes + MaxSurfaces * Stride + Stride) / 4u, 0u); + auto* texels = reinterpret_cast((reinterpret_cast(storage.data()) + Stride - 1u) & ~std::uintptr_t{Stride - 1u}); + const auto base = static_cast(reinterpret_cast(texels)); + const auto blockBytes = static_cast(SurfaceBytes + (MaxSurfaces - 1u) * Stride); + { + GuestAllocations::Mutation mutation; + mutation.Add(texels, blockBytes, true, true, true); + } + const auto release = [&] { + device->WaitIdle(); + AgcDriver::Graphics::ClearCachedTextures(device->Device()); + GuestAllocations::Mutation mutation; + mutation.Remove(texels); + }; + try { + Record(*device, base); + auto retained = StorageTexture::FindLive(base, SurfaceBytes); + Require(retained != nullptr, "surface 0 has no cached storage image"); + const auto retainedBytes = retained->BudgetBytes(); + auto* recorder = Recorder::Active(); + Require(recorder != nullptr && recorder->Recording(), "the surface dispatch was not left in an open batch"); + const auto beforeEviction = recorder->OpenKeptBytes(); + const auto capacity = static_cast(TestBudget / retainedBytes); + Require(capacity + 1u < MaxSurfaces, "the eviction set exceeds the test storage"); + retained.reset(); + for (std::uint32_t surface = 1; surface <= capacity; ++surface) Record(*device, base + surface * Stride); + Require(StorageTexture::FindLive(base, SurfaceBytes) == nullptr, "surface 0 remained cached after the storage budget was exceeded"); + Require(recorder->OpenKeptBytes() >= beforeEviction + retainedBytes, "an evicted storage image held by a batch without a shared resource-cache entry was not charged to that batch"); + } catch (...) { + release(); + throw; + } + release(); + return 0; } -int main() { +} + +int main(int argc, char** argv) { try { + if (argc == 2 && std::string(argv[1]) == "--retained-batch") return RetainedBatch(); #ifdef _WIN32 Require(_putenv_s("APS5_TEXTURE_CACHE_MIB", "32") == 0, "cannot set the test cache budget"); #else From 68f0acf4f72d87c0b6a6f1b4f7b9a1273c2e9fcc Mon Sep 17 00:00:00 2001 From: fogydes Date: Sat, 17 Oct 2026 15:25:46 +0530 Subject: [PATCH 112/131] fix(libSceJobManager): prevent wait deadlock on node exhaustion and clean up lifecycle --- core/libs/prx/libSceJobManager/Export.cpp | 27 +++++++++---------- core/libs/tests/GuestJobManager.cpp | 33 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/core/libs/prx/libSceJobManager/Export.cpp b/core/libs/prx/libSceJobManager/Export.cpp index 4c03e4254e..155a95220d 100644 --- a/core/libs/prx/libSceJobManager/Export.cpp +++ b/core/libs/prx/libSceJobManager/Export.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -198,7 +199,6 @@ private: std::uint32_t outstanding = 0; std::size_t cursor = 0; std::deque items; - std::mutex waiters; std::condition_variable passed; Label label{}; std::uint64_t labelSeen = 0; @@ -221,7 +221,6 @@ public: struct Manager { std::mutex lock; std::condition_variable work; - std::condition_variable nodeFreed; bool running = false; std::uint32_t blockedWaiters = 0; std::uint32_t freeNodes = 0; @@ -251,7 +250,6 @@ bool TakeNode(Manager& manager) { void ReturnNodes(Manager& manager, std::uint32_t count) { manager.freeNodes += count; - manager.nodeFreed.notify_all(); } int Sequence::Latch(int code) { @@ -303,8 +301,8 @@ void Sequence::Advance() { continue; } bool* const waiter = item.passed; - const auto retired = static_cast(cursor) + 1; - items.erase(items.begin(), items.begin() + static_cast(retired)); + const auto retired = static_cast(cursor); + items.erase(items.begin(), items.begin() + static_cast(retired + 1)); cursor = 0; commands.clear(); batchStarted = false; @@ -439,7 +437,8 @@ int Sequence::Wait() { int Sequence::WaitAndClose(std::uint32_t index) { std::unique_lock guard(manager->lock); const int result = WaitLocked(guard); - ReturnNodes(*manager, static_cast(items.size()) + 1); + 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(); cursor = 0; @@ -457,13 +456,7 @@ int Sequence::WaitLocked(std::unique_lock& guard) { } else if (submitted.waits++ >= limits.waits) { result = Latch(SequenceLimitExceeded); } - if (manager->freeNodes == 0 && result == 0) result = Latch(SequenceNoNodes); ++manager->blockedWaiters; - manager->nodeFreed.wait(guard, [this] { return manager->freeNodes != 0; }); - --manager->freeNodes; - guard.unlock(); - const std::lock_guard serial(waiters); - guard.lock(); bool done = false; items.push_back({ItemType::Wait, this, nullptr, nullptr, 0, 0, 0, &done}); Advance(); @@ -700,10 +693,16 @@ void APS5_VABI _ZN3sce3Job10JobManagerC2Ev(void* self) { Implementation(self) = nullptr; } -void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void*) { +int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void* self); + +void APS5_VABI _ZN3sce3Job10JobManagerD1Ev(void* self) { + if (Implementation(self) != nullptr) { + static_cast(_ZN3sce3Job10JobManager8shutdownEv(self)); + } } -void APS5_VABI _ZN3sce3Job10JobManagerD2Ev(void*) { +void APS5_VABI _ZN3sce3Job10JobManagerD2Ev(void* self) { + _ZN3sce3Job10JobManagerD1Ev(self); } std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams* params) { diff --git a/core/libs/tests/GuestJobManager.cpp b/core/libs/tests/GuestJobManager.cpp index e82703be18..7258ae6694 100644 --- a/core/libs/tests/GuestJobManager.cpp +++ b/core/libs/tests/GuestJobManager.cpp @@ -354,6 +354,34 @@ static void LimitsCase() { while (accepted < 0x3ff && (result = sequence->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++accepted; Require(result == NoNodes && accepted == 33); Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == Invalid); + Require(sequence->Wait() == Invalid); + Require(factory->Destroy(sequence) == 0); + Require(factory->Create(&sequence, 0x7fff, 0x3ff, 0xff, 10, "recycle") == 0); + for (int cycle = 0; cycle < 5; ++cycle) { + for (int index = 0; index < 33; ++index) { + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + } + Require(sequence->Wait() == 0); + } + int finalAccepted = 0; + int finalResult = 0; + while (finalAccepted < 0x3ff && (finalResult = sequence->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++finalAccepted; + Require(finalResult == NoNodes && finalAccepted == 33); + Require(sequence->Wait() == Invalid); + Require(factory->Destroy(sequence) == 0); + for (int round = 0; round < 5; ++round) { + Require(factory->Create(&sequence, 0x7fff, 0x3ff, 0xff, 2, "recycle_round") == 0); + for (int index = 0; index < 33; ++index) { + Require(sequence->Dispatch(Nothing, nullptr, 1, nullptr) == 0); + } + Require(sequence->Wait() == 0); + int acceptedInRound = 0; + int resultInRound = 0; + while (acceptedInRound < 0x3ff && (resultInRound = sequence->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++acceptedInRound; + Require(resultInRound == NoNodes && acceptedInRound == 33); + Require(sequence->Wait() == Invalid); + Require(factory->Destroy(sequence) == 0); + } Require(Shutdown(manager.storage) == 0); } @@ -418,6 +446,11 @@ static void ShutdownCase() { Require(Shutdown(manager.storage) == NotInitialized); Require(Factories(manager.storage, 0) == nullptr); } + { + Block scopedBlock(SizeFor(2, 2, 8, 2, 2)); + ManagerObject scoped; + Require(Initialize(scoped.storage, scopedBlock.memory, scopedBlock.bytes, 2, 2, nullptr) == 0); + } } int main(int argc, char** argv) { From 5394684198bc90e65afab15bda4240e1fb4b6c46 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 15:02:29 +0000 Subject: [PATCH 113/131] fix(agc): create legal sampled cache placeholders --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 4 +++- .../Graphics/src/PlaceholderImages.cpp | 3 ++- .../tests/execution/SampledCacheBudget.cpp | 14 +++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index fdca183551..d96544e9c7 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1960,12 +1960,14 @@ if(BUILD_TESTING) set_tests_properties(agc_driver_storage_cache_budget PROPERTIES TIMEOUT 30) add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory) set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77) + add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample) + set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) 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) set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30) if(WIN32) - set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() # The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp index cc352d0d9c..6319daa0b2 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp @@ -21,7 +21,8 @@ PlaceholderImages::PlaceholderImages(const Context& context) : context(context) try { std::vector toClear; for (std::uint32_t index = 0; index < images.size(); ++index) { - const auto shape = PlaceholderImageShapeOf(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index); + auto shape = PlaceholderImageShapeOf(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index); + if ((shape.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0 && shape.samples != VK_SAMPLE_COUNT_1_BIT && !context.storageImageMultisample) shape.samples = VK_SAMPLE_COUNT_1_BIT; if (!Supported(context, shape)) continue; auto& image = images[index]; VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index bdd7de4bc6..0f53a9c011 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -50,7 +50,7 @@ constexpr std::uint64_t Ceiling = Budget + Budget / 4u; class Device { public: - explicit Device(bool resourceImages = false) { + explicit Device(bool enableStorageImageMultisample = true) { #ifdef _WIN32 library = SDL_LoadObject("vulkan-1.dll"); #else @@ -96,13 +96,12 @@ public: queue.queueFamilyIndex = family; queue.queueCount = 1; queue.pQueuePriorities = &priority; + VkPhysicalDeviceFeatures supported{}; + function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); VkPhysicalDeviceFeatures enabled{}; enabled.shaderInt64 = VK_TRUE; - if (resourceImages) { - VkPhysicalDeviceFeatures supported{}; - function("vkGetPhysicalDeviceFeatures")(context.physical, &supported); - enabled.shaderStorageImageMultisample = supported.shaderStorageImageMultisample; - } + enabled.shaderStorageImageMultisample = enableStorageImageMultisample && supported.shaderStorageImageMultisample; + context.storageImageMultisample = enabled.shaderStorageImageMultisample == VK_TRUE; address.pNext = &bytes; std::vector extensionsEnabled{VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME, VK_KHR_8BIT_STORAGE_EXTENSION_NAME}; if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME)) { @@ -720,6 +719,7 @@ int main(int argc, char** argv) { const bool memory = argc > 1 && std::string(argv[1]) == "memory"; const std::string mode = argc > 1 ? argv[1] : ""; const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear"; + const bool disableStorageImageMultisample = mode == "without-storage-image-multisample"; #ifdef _WIN32 if (!memory) _putenv_s("APS5_TEXTURE_CACHE_MIB", "1"); #else @@ -727,7 +727,7 @@ int main(int argc, char** argv) { #endif std::unique_ptr device; try { - device = std::make_unique(resources); + device = std::make_unique(!disableStorageImageMultisample); } catch (const std::exception& error) { if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; std::printf("skipped, no usable Vulkan device: %s\n", error.what()); From 75d0e9a7f4ca3b9ddb937ba84177f96cbfd81b07 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 15:46:12 +0000 Subject: [PATCH 114/131] test(videoout): correct first flip pacing assertion --- core/libs/prx/libSceVideoOut/tests/Flip.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 0f991c0c3c..a7072e5e51 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -681,7 +681,7 @@ void testReleaseVblank() { const bool done = cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == 1; }); if (cfg->failure) std::rethrow_exception(cfg->failure); check(done, "the flip did not complete"); - check(cfg->lastFlipVblank >= 3 && cfg->vblankStatus.count >= cfg->lastFlipVblank + 3, "the next flip is not paced from the vblank this flip was released at"); + check(cfg->lastFlipVblank >= 3, "the first flip was released before its configured interval"); } sceVideoOutUnregisterBuffers(handle, 0); sceVideoOutClose(handle); From 7e4ce1ab3d922ca27873f9eea583bfebec3aff21 Mon Sep 17 00:00:00 2001 From: Nahid Delic Date: Wed, 14 Oct 2026 23:44:17 +0200 Subject: [PATCH 115/131] fix(shader): act on the containing dword for unaligned 32-bit buffer atomics The RDNA2 ISA (8.1.7) ignores the two low address bits of dword or wider non-formatted accesses in alignment mode DWORD. MXGP 26 (PPSA30839) sets bits with buffer_atomic_or at odd byte offsets, which recorded an unaligned BDA fault and dropped the atomic. A 32-bit buffer atomic now acts on the dword that holds its address; 64-bit atomics off an 8-byte boundary still fault. Co-Authored-By: Claude Opus 5.5 --- .../libSceAgcDriver/tests/ShaderMemory.cpp | 2 +- .../tests/execution/BufferAtomicUnaligned.cpp | 92 +++++++++++++++++++ .../SpirvBackend/src/SpirvRuntimeBuffer.cpp | 34 ++++--- docs/dev/debt/shader.md | 1 + 4 files changed, 114 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index 818211166c..0527b07d62 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -4507,7 +4507,7 @@ void verifySpecializedBufferAtomicBindings() { } require(!physical, "specialized buffer atomic: the module addresses physical storage without an address access"); require(int64, "specialized buffer atomic: the fault address is 64-bit and the module does not declare Int64"); - require(faultFunction != 0 && faultCalls == 1, "specialized buffer atomic: an unaligned atomic does not record exactly one fault"); + require(faultCalls == 0, "specialized buffer atomic: a 32-bit atomic records an unaligned fault instead of acting on its dword"); const std::array shaderOnly{spv::CapabilityShader}; expectFailure([&] { static_cast(compile(shaderOnly)); }, "buffer atomic fault addresses require shaderInt64", "specialized buffer atomic: the program compiled without shaderInt64"); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp new file mode 100644 index 0000000000..0cfcfe9c2f --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 32; +constexpr std::uint32_t Sentinel = 0x5eed0001u; +constexpr std::size_t OffsetWord = 2; +alignas(256) std::array Buffer{}; +alignas(256) std::array Output{}; + +alignas(256) auto Code = std::to_array({ + 0x34020082, + 0x7e040281, + 0xe0c85000, 0x80000201, + 0xbf8c3f70, + 0xe0701000, 0x80010201, + 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}; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t offset) { + for (std::uint32_t lane = 0; lane < Threads; ++lane) Buffer[lane] = lane * 100u; + Buffer[Threads] = Sentinel; + Output.fill(0xdeadbeefu); + Code[OffsetWord] = 0xe0c85000u | offset; + std::vector userData(8, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(Buffer.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(buffer.begin(), buffer.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; + const auto result = ShaderRecompiler::Recompile(request); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t offset) { + const auto what = "buffer_atomic_add at byte offset " + std::to_string(offset) + ": lane "; + for (std::uint32_t lane = 0; lane < Threads; ++lane) { + Require(Output[lane] == lane * 100u, what + std::to_string(lane) + " returned " + std::to_string(Output[lane]) + ", expected " + std::to_string(lane * 100u)); + Require(Buffer[lane] == lane * 100u + 1u, what + std::to_string(lane) + " left dword " + std::to_string(Buffer[lane]) + ", expected " + std::to_string(lane * 100u + 1u)); + } + Require(Buffer[Threads] == Sentinel, what + "the dword after the last lane changed"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + if (!TargetHasCapability(device->Target(), spv::CapabilityInt64)) { + std::puts("skipped, the device has no shaderInt64"); + return VulkanTestSkipped; + } + for (std::uint32_t offset = 0; offset < 4u; ++offset) { + Run(*device, offset); + Check(offset); + } + std::puts("buffer atomic unaligned tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp index 7820a803c3..fa019cafce 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvRuntimeBuffer.cpp @@ -811,33 +811,39 @@ std::uint32_t EmitRuntimeBufferAtomic(SpirvValueEmitContext& context, const IrVa if (!context.Memory(instruction).gpuDescriptor) { const auto storage = directAccess(context, instruction, bits == 64u); const auto byte = EmitAddU32(state, access.guest, storage.byteOffset); - const auto unaligned = nonzero(state, EmitAndConstant(state, byte, bits / 8u - 1u)); - const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), access.guest)); - EmitIfCondition(state, unaligned, [&] { - const auto high = Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), guest, BdaConstant(state, 32u))); - RecordBdaFaultWords(state, Unary(state, spv::OpUConvert, TypeU32(state), guest), high, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Unaligned); - }); - StopBdaInvocationIf(state, unaligned); const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byte, ConstantU32(state, bits == 64u ? 3u : 2u)); - return EmitValueOrDefaultIfCondition(state, both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), EmitMemoryElementInBounds(state, storage, index)), type, zero, [&] { + auto valid = EmitMemoryElementInBounds(state, storage, index); + if (bits == 64u) { + const auto unaligned = nonzero(state, EmitAndConstant(state, byte, bits / 8u - 1u)); + const auto guest = Binary(state, spv::OpIAdd, TypeScalarU64(state), resource.base, Unary(state, spv::OpUConvert, TypeScalarU64(state), access.guest)); + EmitIfCondition(state, unaligned, [&] { + const auto high = Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), guest, BdaConstant(state, 32u))); + RecordBdaFaultWords(state, Unary(state, spv::OpUConvert, TypeU32(state), guest), high, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction), BdaAbi::FaultReason::Unaligned); + }); + StopBdaInvocationIf(state, unaligned); + valid = both(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), valid); + } + return EmitValueOrDefaultIfCondition(state, valid, type, zero, [&] { const auto pointer = EmitStorageBufferElementPointer(state, storage, index, bits == 64u ? TypeStorageBufferU64ElementPointer(state) : TypeStorageBufferElementPointer(state)); return operation(pointer); }); } - const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, bits / 8u - 1u)), BdaConstant(state, 0u)); - faultIf(context, instruction, unaligned, access.guest, bits / 8u, BdaAbi::FaultReason::Unaligned); - return EmitValueOrDefaultIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), type, zero, [&] { + const auto atomicAt = [&](std::uint32_t guest) { const auto physical = state.module.AllocateId(); - state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaAtomicPointerFunction, access.guest, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction)); + state.module.AddFunction(spv::OpFunctionCall, TypeScalarU64(state), physical, state.bdaAtomicPointerFunction, guest, ConstantU32(state, bits / 8u), BdaInstructionPc(state, instruction)); const auto mapped = Binary(state, spv::OpINotEqual, TypeBool(state), physical, BdaConstant(state, 0u)); return EmitValueOrDefaultIfCondition(state, mapped, type, zero, [&] { const auto pointer = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToPtr, TypePointer(state, spv::StorageClassPhysicalStorageBuffer, scalar), pointer, physical); const auto result = operation(pointer); - state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, access.guest); + state.module.AddFunction(spv::OpFunctionCall, state.module.Type(spv::OpTypeVoid), state.module.AllocateId(), state.bdaNoteWriteFunction, guest); return result; }); - }); + }; + if (bits == 32u) return atomicAt(Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, ~std::uint64_t{3u}))); + const auto unaligned = Binary(state, spv::OpINotEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeScalarU64(state), access.guest, BdaConstant(state, bits / 8u - 1u)), BdaConstant(state, 0u)); + faultIf(context, instruction, unaligned, access.guest, bits / 8u, BdaAbi::FaultReason::Unaligned); + return EmitValueOrDefaultIfCondition(state, Unary(state, spv::OpLogicalNot, TypeBool(state), unaligned), type, zero, [&] { return atomicAt(access.guest); }); }); }); } diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index 28cd206d18..e65c058fc0 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -133,6 +133,7 @@ - [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. - [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. 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 96c692c887338e3e3ab02061845557e829372661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 18 Oct 2026 17:36:42 +0200 Subject: [PATCH 116/131] test(agc): expect 32-bit buffer atomics off a DWORD boundary to act on their containing dword Co-Authored-By: Claude Sonnet 5.5 --- .../tests/execution/BufferUnalignedBase.cpp | 10 +++++++--- docs/dev/debt/shader.md | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp index c3520e8346..bc07a0cdbf 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferUnalignedBase.cpp @@ -244,9 +244,13 @@ void RunAtomic(AgcDriver::VulkanDevice& device, std::uint32_t misalignment) { for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, run + ": lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill)); } -void RunAtomicFault(AgcDriver::VulkanDevice& device) { +void RunAtomicContainingDword(AgcDriver::VulkanDevice& device) { DispatchAtomic(device, 2u, 0u); - for (std::uint32_t word = 0; word < Output.size(); ++word) Require(Output[word] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", but an atomic off a DWORD boundary faults the wave on RDNA2"); + for (std::uint32_t word = 0; word < 128u; ++word) { + const auto expected = word >= 1u && word < Lanes ? Fill + word + 1u : Fill; + Require(Output[word] == expected, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", expected " + Hex(expected) + ": a 32-bit atomic off a DWORD boundary acts on its containing dword"); + } + for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill)); } } @@ -272,7 +276,7 @@ int main() { RunFormattedFault(*device, "tbuffer_load_format_x 32_UINT", Load32Code, view); RunFormattedFault(*device, "tbuffer_load_format_x 16_16_UINT", Load16_16Code, view); } - RunAtomicFault(*device); + RunAtomicContainingDword(*device); std::puts("buffer unaligned base tests passed"); return 0; } catch (const std::exception& error) { diff --git a/docs/dev/debt/shader.md b/docs/dev/debt/shader.md index e65c058fc0..24d4948928 100644 --- a/docs/dev/debt/shader.md +++ b/docs/dev/debt/shader.md @@ -133,7 +133,7 @@ - [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. - [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. Covered by [BufferAtomicUnaligned.cpp](../../../core/libs/prx/libSceAgcDriver/tests/execution/BufferAtomicUnaligned.cpp). +- [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 fd872264e2e34adecc24737873194c94b238c525 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 16:02:26 +0000 Subject: [PATCH 117/131] fix(agc): omit unavailable storage MSAA placeholders --- .../Graphics/include/PlaceholderImages.hpp | 7 +++++++ .../Graphics/src/PlaceholderImages.cpp | 19 +++++++++---------- .../tests/PlaceholderImages.cpp | 6 ++++++ .../tests/execution/SampledCacheBudget.cpp | 15 +++++++++++++++ 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp index 7e9a4dc18b..71575f622a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp @@ -5,6 +5,7 @@ #include "RuntimeAbi.hpp" #include #include +#include namespace AgcDriver::Graphics { @@ -51,6 +52,12 @@ constexpr PlaceholderImageShape PlaceholderImageShapeOf(std::uint32_t heap) { samples[dimension]}; } +constexpr std::optional PlaceholderImageShapeForDevice(std::uint32_t heap, bool storageImageMultisample) { + const auto shape = PlaceholderImageShapeOf(heap); + if (!storageImageMultisample && (shape.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0 && shape.samples != VK_SAMPLE_COUNT_1_BIT) return std::nullopt; + return shape; +} + class PlaceholderImages { public: explicit PlaceholderImages(const Context& context); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp index 6319daa0b2..68487401c7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/PlaceholderImages.cpp @@ -21,19 +21,18 @@ PlaceholderImages::PlaceholderImages(const Context& context) : context(context) try { std::vector toClear; for (std::uint32_t index = 0; index < images.size(); ++index) { - auto shape = PlaceholderImageShapeOf(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index); - if ((shape.usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0 && shape.samples != VK_SAMPLE_COUNT_1_BIT && !context.storageImageMultisample) shape.samples = VK_SAMPLE_COUNT_1_BIT; - if (!Supported(context, shape)) continue; + const auto shape = PlaceholderImageShapeForDevice(ShaderRecompiler::RuntimeAbi::FirstImageBinding + index, context.storageImageMultisample); + if (!shape || !Supported(context, *shape)) continue; auto& image = images[index]; VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; - info.imageType = shape.type; - info.format = shape.format; + info.imageType = shape->type; + info.format = shape->format; info.extent = {1, 1, 1}; info.mipLevels = 1; info.arrayLayers = 1; - info.samples = shape.samples; + info.samples = shape->samples; info.tiling = VK_IMAGE_TILING_OPTIMAL; - info.usage = shape.usage; + info.usage = shape->usage; info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; Check(context.Function("vkCreateImage")(context.device, &info, nullptr, &image.image), "vkCreateImage placeholder"); @@ -46,9 +45,9 @@ PlaceholderImages::PlaceholderImages(const Context& context) : context(context) Check(context.Function("vkBindImageMemory")(context.device, image.image, image.memory, 0), "vkBindImageMemory placeholder"); VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; viewInfo.image = image.image; - viewInfo.viewType = shape.viewType; - viewInfo.format = shape.format; - viewInfo.subresourceRange = {shape.aspect, 0, 1, 0, 1}; + viewInfo.viewType = shape->viewType; + viewInfo.format = shape->format; + viewInfo.subresourceRange = {shape->aspect, 0, 1, 0, 1}; Check(context.Function("vkCreateImageView")(context.device, &viewInfo, nullptr, &image.view), "vkCreateImageView placeholder"); VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; diff --git a/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp b/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp index e00241e373..f345d9f076 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PlaceholderImages.cpp @@ -65,6 +65,8 @@ void CheckEveryHeap() { continue; } const auto shape = AgcDriver::Graphics::PlaceholderImageShapeOf(heap); + const auto withoutStorageMsaa = AgcDriver::Graphics::PlaceholderImageShapeForDevice(heap, false); + const auto withStorageMsaa = AgcDriver::Graphics::PlaceholderImageShapeForDevice(heap, true); const auto where = " for heap " + std::to_string(heap); const bool storage = resourceClass == ImageResourceClass::Storage; const bool multisampled = dimension == RdnaImageDimension::Dim2DMsaa || dimension == RdnaImageDimension::Dim2DMsaaArray; @@ -73,6 +75,10 @@ void CheckEveryHeap() { Require(shape.format == Format(image), "placeholder format disagrees with the numeric class" + where); Require(shape.viewType == ViewType(dimension), "placeholder view type disagrees with the dimension" + where); Require((shape.samples != VK_SAMPLE_COUNT_1_BIT) == multisampled, "placeholder sample count disagrees with multisampling" + where); + Require(withStorageMsaa.has_value(), "an enabled device lost a placeholder image shape" + where); + Require(withoutStorageMsaa.has_value() == !(storage && multisampled), "a device without storage-image MSAA exposes an unsupported placeholder" + where); + if (withoutStorageMsaa) Require(withoutStorageMsaa->samples == shape.samples && withoutStorageMsaa->viewType == shape.viewType && withoutStorageMsaa->usage == shape.usage, + "a device-specific placeholder changed the typed heap shape" + where); Require(((shape.features & VK_FORMAT_FEATURE_STORAGE_IMAGE_ATOMIC_BIT) != 0) == atomic, "placeholder format features disagree with atomics" + where); heaps.insert(heap); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp index 0f53a9c011..881867d747 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/SampledCacheBudget.cpp @@ -734,6 +734,21 @@ int main(int argc, char** argv) { return VulkanTestSkipped; } auto context = device->GetContext(); + if (disableStorageImageMultisample) { + PlaceholderImages placeholders(context); + for (std::uint32_t heap = ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding; + heap < ShaderRecompiler::RuntimeAbi::FirstImageBinding + ShaderRecompiler::RuntimeAbi::ImageBindingCount; ++heap) { + const auto shape = PlaceholderImageShapeOf(heap); + if (shape.samples == VK_SAMPLE_COUNT_1_BIT) continue; + bool refused = false; + try { + placeholders.View(heap); + } catch (const std::runtime_error&) { + refused = true; + } + Require(refused, "a device without storage-image MSAA returned a placeholder for typed multisample storage heap " + std::to_string(heap)); + } + } TextureDetiler detiler(context); context.detiler = &detiler; if (resources) { From bb39eba819c0e4e2ccd0467ab2a47378ebe9075b Mon Sep 17 00:00:00 2001 From: Maz Date: Wed, 14 Oct 2026 14:55:34 +0400 Subject: [PATCH 118/131] fix(agc): decode COLOR_8_8 UINT as R8G8_UINT --- core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/Graphics.cpp | 12 ++++++++++++ docs/dev/debt/libSceAgcDriver.md | 1 + 3 files changed, 14 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 38d058aff3..aa903ba8fa 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -797,6 +797,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, const auto mapping = static_cast(alternate ? 0xecu : 0xe4u); if (number == unorm) return {VK_FORMAT_R8G8_UNORM, 2, mapping}; if (number == snorm) return {VK_FORMAT_R8G8_SNORM, 2, mapping}; + if (number == uint) return {VK_FORMAT_R8G8_UINT, 2, mapping}; return fail(); } case 4: diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index a047c301a7..efbc2854b8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -2787,6 +2787,17 @@ void cmaskTests() { Require(texels(0x5a5a5a5au), "a refused pass changed the texels of a DCC target"); } +void uint8x2TargetTests() { + auto queue = makeState(); + queue.context[0x1c5] = 7; + queue.context[0x31c] = (queue.context[0x31c] & ~0x1ffcu) | (3u << 2u) | (4u << 8u); + const auto state = AgcDriver::Graphics::DecodeState(queue); + Require(state.color.format == VK_FORMAT_R8G8_UINT && state.color.elementBytes == 2 && state.color.componentMapping == 0xe4u && state.color.uintExport, "an 8_8 UINT color target was rejected"); + queue.context[0x31c] = (queue.context[0x31c] & ~0x1800u) | (1u << 11u); + const auto alt = AgcDriver::Graphics::DecodeState(queue); + Require(alt.color.format == VK_FORMAT_R8G8_UINT && alt.color.componentMapping == 0xecu, "an 8_8 UINT SWAP_ALT color target was rejected"); +} + void uint8x4TargetTests() { auto queue = makeState(); queue.context[0x1c5] = 7; @@ -8877,6 +8888,7 @@ int main(int argc, char** argv) { metadataPassTests(); cmaskTests(); uint16ExportTests(); + uint8x2TargetTests(); uint8x4TargetTests(); msaaEnableClearDepthTests(); multisampleTests(); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 2c2cdc7f2c..0cf731ad3a 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -66,6 +66,7 @@ - [Per-face depth bias](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order. - [Draw recipes with depth targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): direct draw recipes record and replay with active depth targets, matching the bound depth surface view by its target view identity; altered depth surface addresses or incompatible view handles trigger a recipe miss and pipeline rebuild. - [Partial rect-list primitives](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): a rect-list draw whose vertex or index count is not a multiple of 3 draws its complete rectangles and leaves out the 1 or 2 vertices after them, because rect lists are drawn as Vulkan patch lists of 3 control points, which generate [⌊vertexCount/3⌋ patches](https://registry.khronos.org/vulkan/specs/latest/html/vkspec.html#drawing-patch-lists). What the console draws for the incomplete rectangle is not measured; Palworld (PPSA23126) draws 4 vertices × 9 instances as a rect list when its character creation opens. +- [8_8 UINT color targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) decode as `R8G8_UINT` with the same component-swap mapping as 8_8 UNORM/SNORM (`0xe4` / `0xec`). Follows the existing 8 and 8_8_8_8 UINT paths; not measured on the console. RetroArch (PPSA99169) uses it during menu startup. - [Separate alpha blend into 8_8 SWAP_ALT targets](../../../core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp) (libSceAgcDriver): the second channel of a two-channel target with `COMP_SWAP` 1 holds the alpha export, but Vulkan blends it with the color equation of the R8G8 attachment, so a draw whose alpha equation differs from its color equation is recorded twice, the first channel with the color equation and the second with the alpha factors and operation moved into the color equation. The vertex stages run twice, so a vertex shader that writes memory writes twice, and occlusion counts include both passes. Draws that write depth or stencil, pixel shaders that write memory and factors of the destination alpha throw. That the hardware blends the second channel with the alpha equation is taken from Mesa's `si_translate_colorswap` and radeonsi's use of it for luminance-alpha formats, not measured on hardware. - [Unexported pixel inputs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver): a pixel input whose `SPI_PS_INPUT_CNTL` parameter the previous stage does not export is programmed as a default-value input (`OFFSET` bit 5, `DEFAULT_VAL` 0, FP16 high half too) and reads (0, 0, 0, 0), as radeonsi's `SI_PS_INPUT_CNTL_UNUSED` does and as rect-list draws already do. The console's value for an unwritten parameter cache entry is not measured on hardware. - [Per-sample pixel shading](../../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver): `PA_SC_MODE_CNTL_1.PS_ITER_SAMPLE` (bit 16, [PS5_Mesa GFX10.3 registers](https://github.com/mihawk-99/PS5_Mesa/blob/0b2d6d1a61d9bbf89cf8beb88a696144f67c61f8/src/amd/registers/gfx103.json)) on a multisampled draw enables Vulkan sample shading with a minimum rate of 1.0, so the pixel program runs once per sample. The draw throws when the device lacks `sampleRateShading`. On a single-sample draw the bit has no effect and is dropped. That the console runs the pixel program once per covered sample, and what it does with the other multisample registers in that mode, is taken from the register name and Vulkan's definition, not measured on hardware. From 57e52c1f0353a963f2e5b1a605357872bcfa021b Mon Sep 17 00:00:00 2001 From: Karim Masmoudi <125830216+Kernel-Hunter@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:29:01 +0100 Subject: [PATCH 119/131] fix(relinker): report processor 0 from RDTSCP under --to-rosetta Rosetta sets RDTSCP's ECX to its constant TSC_AUX, 0xAAAAAAAB, which is not a processor index. --to-rosetta already makes RDPID return processor 0 for the same reason, so code reading the processor through RDTSCP saw a different value from code reading it through RDPID. --to-rosetta now moves RDTSCP into an optional stub that runs the instruction unchanged and then loads 0 into ECX with a MOV, which leaves the counter in EDX:EAX and the flags as they were. A site with no movable instruction after it for the jump is kept, as for RDPID. The Intel target and a plain relink are unchanged. --- .../x86/Amd64OnlySubstitutionTable.hpp | 1 + .../codegen/x86/DecodedInstruction.hpp | 1 + .../src/x86/Amd64OnlyInstructionMatcher.cpp | 15 ++++++- .../codegen/src/x86/DecodedInstruction.cpp | 17 +++++++ .../codegen/tests/Amd64OnlyConverterTests.cpp | 44 ++++++++++++++++++- docs/dev/debt/relinker.md | 2 +- docs/user/USAGE.md | 2 +- 7 files changed, 77 insertions(+), 5 deletions(-) diff --git a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp index 41948aec85..f82d8a19f9 100644 --- a/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp +++ b/core/relinker/codegen/include/codegen/x86/Amd64OnlySubstitutionTable.hpp @@ -40,6 +40,7 @@ inline constexpr Entry kRdpru = {"RDPRU", nullptr, 0}; inline constexpr Entry kMcommit = {"MCOMMIT", nullptr, 0}; inline constexpr Entry kRdseed = {"RDSEED", nullptr, 0}; inline constexpr Entry kRdpid = {"RDPID", nullptr, 0}; +inline constexpr Entry kRdtscp = {"RDTSCP", nullptr, 0}; inline constexpr Entry kClwb = {"CLWB", nullptr, 0}; inline constexpr Entry kWideStore = {"256-bit store", nullptr, 0}; inline constexpr Entry kScalarCompare = {"scalar compare", nullptr, 0}; diff --git a/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp index 798128e36c..4176cc4cff 100644 --- a/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp +++ b/core/relinker/codegen/include/codegen/x86/DecodedInstruction.hpp @@ -25,6 +25,7 @@ struct DecodedInstruction { [[nodiscard]] bool IsMovntsd() const; [[nodiscard]] bool IsRdseed() const; [[nodiscard]] bool IsRdpid() const; + [[nodiscard]] bool IsRdtscp() const; [[nodiscard]] bool IsClwb() const; private: diff --git a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp index 7b6175bdff..c9128ecbaf 100644 --- a/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp +++ b/core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp @@ -134,6 +134,13 @@ std::vector _rdpidReplacement(const DecodedInstruction& instr) { return bytes; } +std::vector _rdtscpReplacement(const DecodedInstruction& instr) { + std::vector bytes(instr.Data, instr.Data + instr.Length); + bytes.push_back(0xB9); + bytes.insert(bytes.end(), 4, 0); + return bytes; +} + bool _validWait(const DecodedInstruction& instr) { const auto opcode = instr.Data + instr.OpcodeOffset(); return instr.Length <= kMaxInstructionLength && std::find(instr.Data, opcode, X64OpcodeConstants::PrefixLock) == opcode; @@ -229,6 +236,8 @@ std::optional Amd64OnlyInstructionMatcher::_matchRosetta(const D return _inPlace(kClwb, instr.Length, {}); if (instr.IsRdpid()) return _trampoline(kRdpid.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdpidReplacement(instr)); }), true); + if (instr.IsRdtscp()) + return _trampoline(kRdtscp.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { body.Raw(_rdtscpReplacement(instr)); }), true); if (const auto wide = DecodeVexWideStore(instr.Data, instr.Length)) return _trampoline(kWideStore.Name, instr.Length, _outOfLine(instr.Length, trailing, [&](StubBodyBuilder& body) { _wideStoreLowering.EmitOutOfLine(body, *wide); }), false, true); if (const auto compare = DecodeScalarCompare(instr.Data, instr.Length)) @@ -268,6 +277,10 @@ const char* Amd64OnlyInstructionMatcher::_emitOutOfLine(StubBodyBuilder& body, c EmitAndn(body, *andn); return "ANDN"; } + if (_target == Amd64OnlyTarget::Rosetta && instr.IsRdtscp()) { + body.Raw(_rdtscpReplacement(instr)); + return kRdtscp.Name; + } if (const auto wide = _target == Amd64OnlyTarget::Rosetta ? DecodeVexWideStore(instr.Data, instr.Length) : std::nullopt) { _wideStoreLowering.EmitOutOfLine(body, *wide); return kWideStore.Name; @@ -304,7 +317,7 @@ std::optional Amd64OnlyInstructionMatcher::MatchSequence( } _moveTrailing(body, trailing); const DecodedInstruction first{instructions.front().data(), instructions.front().size()}; - const bool optional = DecodeReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && first.IsRdpid()); + const bool optional = DecodeReciprocal(first.Data, first.Length).has_value() || (_target == Amd64OnlyTarget::Rosetta && (first.IsRdpid() || first.IsRdtscp())); return _trampoline(name, instructions.front().size(), body.Finish(), optional); } diff --git a/core/relinker/codegen/src/x86/DecodedInstruction.cpp b/core/relinker/codegen/src/x86/DecodedInstruction.cpp index 3bdb906efe..d54ce541bc 100644 --- a/core/relinker/codegen/src/x86/DecodedInstruction.cpp +++ b/core/relinker/codegen/src/x86/DecodedInstruction.cpp @@ -292,6 +292,23 @@ bool DecodedInstruction::IsRdpid() const { ((modrm >> ModRmModShift) & ModRmModMask) == ModRmModRegister && ((modrm >> ModRmRegShift) & ModRmRegMask) == 7; } +bool DecodedInstruction::IsRdtscp() const { + bool operandSizeOverride = false; + bool repnePrefix = false; + bool repPrefix = false; + const std::size_t pos = _skipPrefixesAndRex(&operandSizeOverride, &repnePrefix, &repPrefix); + + if (operandSizeOverride || repnePrefix || repPrefix) { + return false; + } + + if (pos + 2 >= Length) { + return false; + } + + return Data[pos] == TwoByteOpcodeEscape && Data[pos + 1] == TwoByteGrp7 && Data[pos + 2] == 0xF9; +} + bool DecodedInstruction::IsClwb() const { bool operandSizeOverride = false; bool repnePrefix = false; diff --git a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp index 6fbecaf06b..89bc80f07d 100644 --- a/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp +++ b/core/relinker/codegen/tests/Amd64OnlyConverterTests.cpp @@ -655,8 +655,8 @@ void rosettaSubstitutions() { const auto intel = Codegen::MakeAmd64OnlyInstructionMatcher(); const auto rosetta = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); const auto match = [](const auto& matcher, const Bytes& bytes) { return matcher->Match(bytes.data(), bytes.size()); }; - for (const auto& bytes : {Bytes{0x0F, 0xC7, 0xF8}, Bytes{0xF3, 0x0F, 0xC7, 0xF8}, Bytes{0x66, 0x0F, 0xAE, 0x30}}) - require(!match(intel, bytes).has_value(), "RDSEED, RDPID or CLWB was converted for an Intel host"); + for (const auto& bytes : {Bytes{0x0F, 0xC7, 0xF8}, Bytes{0xF3, 0x0F, 0xC7, 0xF8}, Bytes{0x66, 0x0F, 0xAE, 0x30}, Bytes{0x0F, 0x01, 0xF9}}) + require(!match(intel, bytes).has_value(), "RDSEED, RDPID, CLWB or RDTSCP was converted for an Intel host"); const auto rdseed = match(rosetta, {0x48, 0x0F, 0xC7, 0xF9}); require(rdseed && rdseed->Lowering == Amd64OnlyLowering::InPlace && rdseed->ReplacementBytes == Bytes{0x48, 0x0F, 0xC7, 0xF1} && rdseed->InstructionName == "RDSEED", "RDSEED was not rewritten to RDRAND"); const auto rdseed16 = match(rosetta, {0x66, 0x41, 0x0F, 0xC7, 0xFA}); @@ -669,6 +669,11 @@ void rosettaSubstitutions() { const auto rdpid = match(rosetta, {0xF3, 0x41, 0x0F, 0xC7, 0xFB}); require(rdpid && rdpid->Lowering == Amd64OnlyLowering::Trampoline && rdpid->Optional && rdpid->InstructionName == "RDPID", "RDPID was not lowered through an optional stub"); require(rdpid->ReturnBranchOffset == 6 && Bytes(rdpid->StubBody.begin(), rdpid->StubBody.begin() + 7) == Bytes{0x41, 0xBB, 0, 0, 0, 0, 0xE9}, "RDPID r11 stub does not load 0 before returning"); + const auto rdtscp = match(rosetta, {0x0F, 0x01, 0xF9}); + require(rdtscp && rdtscp->Lowering == Amd64OnlyLowering::Trampoline && rdtscp->Optional && rdtscp->InstructionName == "RDTSCP", "RDTSCP was not lowered through an optional stub"); + require(rdtscp->ReturnBranchOffset == 8 && Bytes(rdtscp->StubBody.begin(), rdtscp->StubBody.begin() + 9) == Bytes{0x0F, 0x01, 0xF9, 0xB9, 0, 0, 0, 0, 0xE9}, "RDTSCP stub does not load 0 into ECX after reading the counter"); + require(!match(rosetta, {0x0F, 0x01, 0xF8}).has_value(), "SWAPGS (0F 01 F8) was taken for RDTSCP"); + require(!match(rosetta, {0x66, 0x0F, 0x01, 0xF9}).has_value(), "RDTSCP with an operand size prefix was lowered"); } void wideStoreSplit() { @@ -1745,6 +1750,40 @@ void immediateFormExecution() {} void ripRelativeExecution() {} #endif +#if defined(__x86_64__) && (defined(__linux__) || defined(__APPLE__)) +void rdtscpExecution() { + const auto matcher = Codegen::MakeAmd64OnlyInstructionMatcher(Codegen::Amd64OnlyTarget::Rosetta); + const Bytes site = {0x0F, 0x01, 0xF9}; + const auto match = matcher->Match(site.data(), site.size()); + require(match && match->Lowering == Codegen::Amd64OnlyLowering::Trampoline, "RDTSCP 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()); + std::uint64_t low = 0; + std::uint64_t high = 0; + std::uint64_t processor = 0; + std::uint8_t carry = 0; + asm volatile( + "mov $-1, %%rcx\n\t" + "sub $128, %%rsp\n\t" + "stc\n\t" + "call *%[code]\n\t" + "setc %[carry]\n\t" + "add $128, %%rsp\n\t" + : "=&a"(low), "=&d"(high), "=&c"(processor), [carry] "=&q"(carry) + : [code] "r"(code) + : "memory", "cc"); + munmap(code, 4096); + require(((high << 32) | low) != 0, "RDTSCP stub did not read the time stamp counter"); + require(processor == 0, "RDTSCP stub left a value other than processor 0 in RCX"); + require(carry == 1, "RDTSCP stub changed the flags"); +} +#else +void rdtscpExecution() {} +#endif + void scannerZeroTail() { const auto scanner = Codegen::MakeInstructionScanner(); const Bytes code{0xC3, 0x00, 0x00, 0x00}; @@ -1786,6 +1825,7 @@ int main() { converterRosettaScalarCompare(); rosettaLegacyScalarCompares(); converterRosettaLegacyScalarCompare(); + rdtscpExecution(); wideStoreSplit(); converterWideStoreWindows(); converterJumpTableTargets(); diff --git a/docs/dev/debt/relinker.md b/docs/dev/debt/relinker.md index 06478ff3d9..cd07d6bf0d 100644 --- a/docs/dev/debt/relinker.md +++ b/docs/dev/debt/relinker.md @@ -18,7 +18,7 @@ - In executables and guest modules with a `PT_SCE_DYNLIBDATA` segment, the [code analysis](../../../core/relinker/relinker/src/analysis/CodeInstructionCollector.cpp) of the Windows and macOS outputs follows code only from the end of `PT_INTERP`, or of the `/libexec/ld-elf.so.1` string that begins the segment when no `PT_INTERP` covers it, as in OpenOrbis modules, to the end of the PLT after the last function of the unwind table, the end the pipeline's scans above use; a guest module with PLT relocations and no PLT after its last unwind function fails as the executable does. Code after that PLT is not analyzed: a TLS access in it is not rewritten, an entry point or pointer into it is ignored, and a function symbol or init/fini array entry pointing into it fails the conversion. - A relocation against a dynamic symbol entry that is all zero, like the null symbol, takes the value 0 plus its addend, as the gABI defines for `STN_UNDEF` ([pipeline](../../../core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp)). The Linux output imports such an entry as an unnamed weak symbol, which the host loader resolves to 0; the Windows and macOS outputs write the value into the slot ([relocation builder](../../../core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp)). What the console's loader does with such an entry is not measured. OliSe Player (PPSA01153) has seven of them, and its code calls through those slots only when they are not 0 ([#4437](https://github.com/boykopovar/AnyPS5/issues/4437)). - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. -- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB; the same value reaches RDTSCP's ECX, which is not converted. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. +- `--to-rosetta` lowers what Rosetta lacks on top of `--to-intel` ([matcher](../../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp)). AVX2, FMA, BMI1/2, F16C and MOVBE were observed to execute on an M5 Max without being reported through CPUID; this is not a guarantee for other Rosetta hosts. ANDN and RDRAND fault on an M1 running macOS 15.3.1 and execute on an M1 running macOS 26.6.2, so on the M1 the difference follows the macOS version; on that host BEXTR, BLSI, BLSMSK, BLSR, TZCNT, LZCNT, POPCNT, BZHI, MULX, PDEP, PEXT, RORX, SARX, SHLX, SHRX, MOVBE, FMA, AVX2 and F16C execute as well, its CPUID leaf 1 reports none of MOVBE, FMA, AVX or F16C, and RDSEED, RDPID, CLWB, ADCX and ADOX raise SIGILL. They also execute on an M5 running macOS 26.4, where the rest of BMI1 and BMI2, LZCNT, PREFETCHW, CLFLUSHOPT, AVX, AVX2, FMA, F16C and MOVBE execute as well although CPUID leaf 7 reads zero and leaves 1 and 0x80000001 report none of them, and where RDSEED, CLWB, RDPID, ADCX and ADOX raise an illegal instruction. ANDN is lowered, but RDRAND and other BMI instructions still depend on the host. RDSEED becomes RDRAND and CLWB a NOP. RDPID returns processor 0, and RDTSCP runs unchanged with ECX set to 0 after it, because Rosetta's TSC_AUX is the constant 0xAAAAAAAB (also measured on an M5, macOS 27.0.1). An RDPID or RDTSCP site with no movable instruction after it for the jump is kept; a kept RDTSCP reports 0xAAAAAAAB. ADCX/ADOX fault under Rosetta and are not converted: compilers do not emit them, and code that checks CPUID sees no ADX. The conversions are covered by the converter tests. ANDN lowering is also executed by an x86-64 test build under Rosetta on the M1; the other conversions were originally checked on an M5 Max, outside this repository's CI. - `--to-intel` replaces the approximate legacy SSE and VEX.128 register forms of RSQRTPS/RCPPS and VRSQRTPS/VRCPPS with [correctly rounded](../../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). Legacy SSE forms preserve the destination's upper YMM bits; VEX forms clear them. The 256-bit, scalar and memory forms keep the native instruction, as does a site shorter than 5 bytes with no movable instruction after it for the jump (5 sites in PPSA21564, counted before a following RIP-relative instruction could move into the stub; not recounted). - `--trace-imports` ([entry stub](../../../core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp)) exists for Windows output only. Only imports bound through a PLT slot, or through a GOT slot whose symbol is a function, are traced; a function reached through any other relocation, or looked up at run time with `sceKernelDlsym`, is not, and neither are weak imports or the imports of modules loaded at run time diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 2138b71355..1a36a518f1 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -62,7 +62,7 @@ All switches are disabled by default. `unused-filter` defaults to `0`; `--rpath` | `--trace-imports` | Pass each function import through the libc.prx call tracer, see [tracing system library calls](#tracing-system-library-calls). Requires `--windows`; conflicts with `--lazy-binding`. | | `--linux-fixed-base
` | Load the Linux executable at this fixed address, given in decimal or hexadecimal (`0x`): every virtual address of the image is offset by it, so virtual address 0 is placed at the address, as the console places an executable. Requires a 64 KiB-aligned address from `0x10000` through `0x7fffffff0000` that is a multiple of every `PT_LOAD` alignment, and an image that ends in the user address space. The executable is written as `ET_EXEC`, without ASLR. Bundled modules keep their normal placement. Conflicts with `--windows`, `--macos` and `--lazy-binding`. | | `--to-intel` | Convert supported AMD-only instructions in the executable and bundled modules. Unsupported instructions or unreachable conversion stubs cause an error. | -| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID returns processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | +| `--to-rosetta` | Convert as `--to-intel` does and also lower what Rosetta lacks: ANDN becomes scalar NOT/AND, RDSEED becomes RDRAND, CLWB a NOP, and RDPID and the ECX of RDTSCP return processor 0. Implies `--to-intel`. Independent of `--macos`: `--macos` alone leaves these instructions as they are. Conflicts with `--windows`. | | `unused-filter=0` | Keep all imported NID references. | | `unused-filter=1` | Filter unused non-PLT imports using control-flow and GOT access analysis, treating address-taken code as reachable; preserve PLT imports. | | `unused-filter=2` | Apply strict unused-import analysis and compact the PLT. Unsupported analysis cases cause an error. | From edbd209a921ff444d0deab7ad0728a96cb1e8428 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Wed, 14 Oct 2026 22:48:44 +0000 Subject: [PATCH 120/131] feat(libkernel): trace semaphore lifecycle under sync logging --- core/libs/prx/libkernel/CMakeLists.txt | 14 +++++- .../prx/libkernel/Semaphore/src/Semaphore.cpp | 24 +++++++++- core/libs/tests/SemaphoreTrace.cpp | 34 ++++++++++++++ core/libs/tests/test_semaphore_trace.py | 46 +++++++++++++++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 core/libs/tests/SemaphoreTrace.cpp create mode 100644 core/libs/tests/test_semaphore_trace.py diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index cfd3eeb569..084b211817 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -695,8 +695,20 @@ if(BUILD_TESTING) configure_windows_unwind(guest_semaphore_tests) add_test(NAME guest_semaphore COMMAND guest_semaphore_tests) set_tests_properties(guest_semaphore PROPERTIES TIMEOUT 20) + if(Python3_Interpreter_FOUND) + add_test_executable(semaphore_trace_tests + ${LIBS_INCLUDE_DIR}/tests/SemaphoreTrace.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp) + target_include_directories(semaphore_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) + find_package(Threads REQUIRED) + target_link_libraries(semaphore_trace_tests PRIVATE Threads::Threads) + add_test(NAME guest_semaphore_trace COMMAND ${Python3_EXECUTABLE} + ${LIBS_INCLUDE_DIR}/tests/test_semaphore_trace.py $) + set_tests_properties(guest_semaphore_trace PROPERTIES TIMEOUT 20) + endif() if(WIN32) - set_tests_properties(guest_semaphore PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_semaphore guest_semaphore_trace PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() add_test_executable(guest_pthread_timed_locks_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadTimedLocks.cpp) diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 7375ae7ff2..725f297e67 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -1,6 +1,8 @@ #include "prx/libkernel/Semaphore/include/Semaphore.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include +#include +#include #include #include @@ -10,6 +12,11 @@ KernelSemaPrivate::KernelSemaPrivate(std::int32_t initCount, std::int32_t maxCou : name(std::move(name)), tokenCount(initCount), initCount(initCount), maxCount(maxCount), isFifo(isFifo) { } +static bool TraceSync() { + static const bool enabled = std::getenv("APS5_TRACE_SYNC") != nullptr; + return enabled; +} + extern "C" { int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t attr, int init, int max, void* opt) { @@ -19,6 +26,7 @@ int APS5_VABI sceKernelCreateSema(KernelSema* sem, const char* name, uint32_t at } *sem = new KernelSemaPrivate(init, max, std::string(name), attr == 1); + if (TraceSync()) std::fprintf(stderr, "[sema] create %p '%s' attr=0x%x init=%d max=%d\n", static_cast(*sem), name, attr, init, max); return KERNEL_SEMA_OK; } @@ -44,7 +52,9 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { if (count > sem->maxCount - sem->tokenCount) { return SCE_KERNEL_ERROR_EINVAL; } + const auto before = sem->tokenCount; sem->tokenCount += count; + if (TraceSync()) std::fprintf(stderr, "[sema] signal %p '%s' count=%d have=%d->%d\n", static_cast(sem), sem->name.c_str(), count, before, sem->tokenCount); sem->condition.NotifyAll(); return KERNEL_SEMA_OK; } @@ -71,20 +81,28 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) } waiterGuard{sem, generation}; const auto waitStart = std::chrono::steady_clock::now(); + const void* caller = __builtin_return_address(0); + if (TraceSync()) std::fprintf(stderr, "[sema] wait begin %p '%s' need=%d have=%d timeout=%lld caller=%p\n", static_cast(sem), sem->name.c_str(), need, sem->tokenCount, time ? static_cast(*time) : -1LL, caller); + const auto traceSyncWaitEnd = [&](int result) { + if (TraceSync()) std::fprintf(stderr, "[sema] wait end %p '%s' result=0x%x have=%d caller=%p\n", static_cast(sem), sem->name.c_str(), result, sem->tokenCount, caller); + }; const auto traceWait = [&](bool timedOut) { - KernelTraceWait_nid_postfix("sema", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); + KernelTraceWait_nid_postfix("sema", caller, static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); }; const auto released = [&] { return sem->tokenCount >= need || sem->deleted || sem->cancelGeneration != generation; }; if (time == nullptr) { sem->condition.Wait(lock, released); traceWait(false); if (sem->deleted) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_EACCES); return SCE_KERNEL_ERROR_EACCES; } if (sem->cancelGeneration != generation) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_ECANCELED); return SCE_KERNEL_ERROR_ECANCELED; } sem->tokenCount -= need; + traceSyncWaitEnd(KERNEL_SEMA_OK); return KERNEL_SEMA_OK; } @@ -93,15 +111,19 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) *time = static_cast(TimedWait::RemainingMicros(deadline)); traceWait(!acquired); if (sem->deleted) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_EACCES); return SCE_KERNEL_ERROR_EACCES; } if (sem->cancelGeneration != generation) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_ECANCELED); return SCE_KERNEL_ERROR_ECANCELED; } if (!acquired) { + traceSyncWaitEnd(SCE_KERNEL_ERROR_ETIMEDOUT); return SCE_KERNEL_ERROR_ETIMEDOUT; } sem->tokenCount -= need; + traceSyncWaitEnd(KERNEL_SEMA_OK); return KERNEL_SEMA_OK; } diff --git a/core/libs/tests/SemaphoreTrace.cpp b/core/libs/tests/SemaphoreTrace.cpp new file mode 100644 index 0000000000..e49947b56e --- /dev/null +++ b/core/libs/tests/SemaphoreTrace.cpp @@ -0,0 +1,34 @@ +#include "prx/libkernel/Semaphore/include/Semaphore.hpp" +#include "prx/libkernel/Time/include/Time.hpp" +#include +#include +#include + +extern "C" void KernelTraceWait_nid_postfix(const char*, const void*, std::uint64_t, bool) {} + +int main() { + KernelSema sem = nullptr; + if (sceKernelCreateSema(&sem, "trace", 0, 0, 1, nullptr) != KERNEL_SEMA_OK) return 1; + + std::atomic result{-1}; + std::thread waiter([&] { result.store(sceKernelWaitSema(sem, 1, nullptr)); }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); + for (;;) { + { + std::lock_guard lock(sem->mutex); + if (sem->waiterCount != 0) break; + } + if (std::chrono::steady_clock::now() >= deadline) { + sceKernelSignalSema(sem, 1); + waiter.join(); + sceKernelDeleteSema(sem); + return 2; + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + const int signaled = sceKernelSignalSema(sem, 1); + waiter.join(); + const int deleted = sceKernelDeleteSema(sem); + return signaled == KERNEL_SEMA_OK && result.load() == KERNEL_SEMA_OK && deleted == KERNEL_SEMA_OK ? 0 : 3; +} diff --git a/core/libs/tests/test_semaphore_trace.py b/core/libs/tests/test_semaphore_trace.py new file mode 100644 index 0000000000..004692b8ce --- /dev/null +++ b/core/libs/tests/test_semaphore_trace.py @@ -0,0 +1,46 @@ +import os +import re +import subprocess +import sys + + +def run(executable, enabled): + environment = os.environ.copy() + environment.pop("APS5_TRACE_SYNC", None) + if enabled: + environment["APS5_TRACE_SYNC"] = "1" + return subprocess.run([executable], capture_output=True, check=False, env=environment, text=True, timeout=15) + + +def main(): + executable = sys.argv[1] + quiet = run(executable, False) + if quiet.returncode != 0 or "[sema]" in quiet.stderr: + raise SystemExit("semaphore test failed or traced without APS5_TRACE_SYNC") + + traced = run(executable, True) + if traced.returncode != 0: + raise SystemExit(traced.stderr or f"semaphore test exited {traced.returncode}") + + lines = traced.stderr.splitlines() + create = next((line for line in lines if "[sema] create " in line and "'trace'" in line), None) + if create is None: + raise SystemExit("semaphore creation trace is missing") + handle_match = re.search(r"\[sema\] create (\S+)", create) + if handle_match is None: + raise SystemExit("semaphore creation trace has no handle") + handle = handle_match.group(1) + expected = { + "signal": f"[sema] signal {handle} 'trace'", + "wait begin": f"[sema] wait begin {handle} 'trace'", + "wait end": f"[sema] wait end {handle} 'trace'", + } + for event, prefix in expected.items(): + if not any(line.startswith(prefix) for line in lines): + raise SystemExit(f"semaphore {event} trace for the created handle is missing") + if not any(line.startswith(expected["wait begin"]) and "timeout=-1" in line for line in lines): + raise SystemExit("indefinite semaphore wait is not identified in the trace") + + +if __name__ == "__main__": + main() From d5b14eaeba1af18ddc791e6208dc681353e1d2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 18 Oct 2026 18:30:22 +0200 Subject: [PATCH 121/131] fix(libkernel): link libc into semaphore_trace_tests for MinGW --- core/libs/prx/libkernel/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 084b211817..ca336be02d 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -702,7 +702,7 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp) target_include_directories(semaphore_trace_tests PRIVATE ${LIBS_INCLUDE_DIR}) find_package(Threads REQUIRED) - target_link_libraries(semaphore_trace_tests PRIVATE Threads::Threads) + target_link_libraries(semaphore_trace_tests PRIVATE Threads::Threads libc) add_test(NAME guest_semaphore_trace COMMAND ${Python3_EXECUTABLE} ${LIBS_INCLUDE_DIR}/tests/test_semaphore_trace.py $) set_tests_properties(guest_semaphore_trace PROPERTIES TIMEOUT 20) From 2ae7896b6d940669001c46a4156335cc34819cdf Mon Sep 17 00:00:00 2001 From: fdfawef324 Date: Wed, 14 Oct 2026 17:11:39 -0400 Subject: [PATCH 122/131] fix(libSceNetCtl): keep the callback argument and deliver the state from sceNetCtlCheckCallback sceNetCtlRegisterCallback and sceNetCtlRegisterCallbackV6 stored only the function and dropped its argument, and sceNetCtlCheckCallback returned 0 without calling anything, so a title that polls sceNetCtlCheckCallback until its callback reports IPOBTAINED never got the event. Each callback slot now keeps the function, its argument and the last event it received; unregistration clears the slot. With no IPv4 callback registered, sceNetCtlCheckCallback returns 0 without querying the host. Otherwise it takes the state from the same host-interface predicate as sceNetCtlGetState, IPOBTAINED (3) or DISCONNECTED (1) (shadPS4 src/core/libraries/network/net_ctl_codes.h L27-29 @15abf68c9b52), marks the IPv4 slots whose last event differs and calls them on the calling thread with their argument after the lock is released, so a callback can unregister itself. Delivery once per state change and slot follows KytyPS5 src/libs/network.cpp @9e41d8320a27 (NetCtlCallbackSlot L3447-3451, NetCtlCheckCallback L3648-3661, NetCtlRegisterCallback L3687-3707); shadPS4 net_ctl_obj.cpp L47-57 delivers on every check instead, and the debt entry records the difference and that KytyPS5 calls the callbacks under its lock. sceNetCtlTerm clears both tables, as KytyPS5 NetCtlTerm (L3625-3633) does. V6 slots keep their argument but get no event, since no source says which call delivers them. NetCtlCallback is now an APS5_VABI pointer, so the Windows build calls the guest function with the System V ABI. Co-Authored-By: Claude Opus 5.5 --- core/libs/SceTypes.hpp | 2 +- core/libs/prx/libSceNetCtl/Export.cpp | 52 +++++++++++--- core/libs/tests/GuestNetCtl.cpp | 99 +++++++++++++++++++++++++++ core/libs/tests/GuestSceNet.cpp | 11 +-- docs/dev/debt/libSceNetCtl.md | 4 ++ 5 files changed, 152 insertions(+), 16 deletions(-) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index 237e9166d9..c5e339bee9 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -1288,7 +1288,7 @@ struct NetCtlIfStat { std::uint32_t reserved[8]; }; -using NetCtlCallback = void (*)(int, void*); +using NetCtlCallback = void (APS5_VABI *)(int, void*); struct HttpEpoll; using HttpEpollHandle = HttpEpoll*; diff --git a/core/libs/prx/libSceNetCtl/Export.cpp b/core/libs/prx/libSceNetCtl/Export.cpp index 2541758590..463c9642ae 100644 --- a/core/libs/prx/libSceNetCtl/Export.cpp +++ b/core/libs/prx/libSceNetCtl/Export.cpp @@ -38,6 +38,8 @@ static constexpr int SCE_NET_CTL_ERROR_INVALID_ADDR = static_cast(0x8041210 static constexpr int SCE_NET_CTL_ERROR_NOT_CONNECTED = static_cast(0x80412108); static constexpr int NET_CTL_STATE_DISCONNECTED = 0; static constexpr int NET_CTL_STATE_IPOBTAINED = 3; +static constexpr int NET_CTL_EVENT_DISCONNECTED = 1; +static constexpr int NET_CTL_EVENT_IPOBTAINED = 3; static constexpr int NET_CTL_INFO_DEVICE = 1; static constexpr int NET_CTL_INFO_ETHER_ADDR = 2; static constexpr int NET_CTL_INFO_MTU = 3; @@ -61,16 +63,22 @@ static constexpr std::uint8_t NET_CTL_ETHER_ADDR[6] = {0x02, 0x50, 0x53, 0x35, 0 static constexpr std::size_t NET_CTL_IPV4_ADDR_STR_LEN = 16; static constexpr int MAX_CALLBACKS = 8; -static std::mutex g_callbackLock; -static NetCtlCallback g_callbacks[MAX_CALLBACKS] = {}; -static NetCtlCallback g_callbacksV6[MAX_CALLBACKS] = {}; +struct CallbackSlot { + NetCtlCallback func = nullptr; + void* arg = nullptr; + int lastEvent = 0; +}; -static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, int* cid) { +static std::mutex g_callbackLock; +static CallbackSlot g_callbacks[MAX_CALLBACKS] = {}; +static CallbackSlot g_callbacksV6[MAX_CALLBACKS] = {}; + +static int RegisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], NetCtlCallback func, void* arg, int* cid) { if (!func || !cid) return SCE_NET_CTL_ERROR_INVALID_ADDR; std::lock_guard lock(g_callbackLock); for (int index = 0; index < MAX_CALLBACKS; ++index) { - if (!callbacks[index]) { - callbacks[index] = func; + if (!callbacks[index].func) { + callbacks[index] = {func, arg, 0}; *cid = index; return 0; } @@ -78,13 +86,21 @@ static int RegisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], NetCtlCa return SCE_NET_CTL_ERROR_CALLBACK_MAX; } -static int UnregisterCallback(NetCtlCallback (&callbacks)[MAX_CALLBACKS], int cid) { +static int UnregisterCallback(CallbackSlot (&callbacks)[MAX_CALLBACKS], int cid) { if (cid < 0 || cid >= MAX_CALLBACKS) return SCE_NET_CTL_ERROR_INVALID_ID; std::lock_guard lock(g_callbackLock); - callbacks[cid] = nullptr; + callbacks[cid] = {}; return 0; } +static bool HasIpv4Callback() { + std::lock_guard lock(g_callbackLock); + for (const auto& slot : g_callbacks) { + if (slot.func) return true; + } + return false; +} + static bool HostHasAddress(int family) { #ifdef _WIN32 static const int socket_status = [] { @@ -324,6 +340,19 @@ static void HostTraffic(std::uint64_t& sent, std::uint64_t& received) { extern "C" { int APS5_VABI sceNetCtlCheckCallback(void) { + if (!HasIpv4Callback()) return 0; + const int event = HostHasAddress(AF_UNSPEC) ? NET_CTL_EVENT_IPOBTAINED : NET_CTL_EVENT_DISCONNECTED; + CallbackSlot due[MAX_CALLBACKS]; + int count = 0; + { + std::lock_guard lock(g_callbackLock); + for (auto& slot : g_callbacks) { + if (!slot.func || slot.lastEvent == event) continue; + slot.lastEvent = event; + due[count++] = slot; + } + } + for (int index = 0; index < count; ++index) due[index].func(event, due[index].arg); return 0; } @@ -390,14 +419,17 @@ int APS5_VABI sceNetCtlInit(void) { } int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback func, void* arg, int* cid) { - return RegisterCallback(g_callbacks, func, cid); + return RegisterCallback(g_callbacks, func, arg, cid); } int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback func, void* arg, int* cid) { - return RegisterCallback(g_callbacksV6, func, cid); + return RegisterCallback(g_callbacksV6, func, arg, cid); } void APS5_VABI sceNetCtlTerm(void) { + std::lock_guard lock(g_callbackLock); + for (auto& slot : g_callbacks) slot = {}; + for (auto& slot : g_callbacksV6) slot = {}; } int APS5_VABI sceNetCtlUnregisterCallback(int cid) { diff --git a/core/libs/tests/GuestNetCtl.cpp b/core/libs/tests/GuestNetCtl.cpp index 5730ecd1a9..0e5a4f6d26 100644 --- a/core/libs/tests/GuestNetCtl.cpp +++ b/core/libs/tests/GuestNetCtl.cpp @@ -19,9 +19,15 @@ #include extern "C" { +int APS5_VABI sceNetCtlCheckCallback(void); int APS5_VABI sceNetCtlGetInfo(int, NetCtlInfo*); int APS5_VABI sceNetCtlGetNatInfo(NetCtlNatInfo*); int APS5_VABI sceNetCtlGetState(int*); +int APS5_VABI sceNetCtlRegisterCallback(NetCtlCallback, void*, int*); +int APS5_VABI sceNetCtlRegisterCallbackV6(NetCtlCallback, void*, int*); +void APS5_VABI sceNetCtlTerm(void); +int APS5_VABI sceNetCtlUnregisterCallback(int); +int APS5_VABI sceNetCtlUnregisterCallbackV6(int); } namespace { @@ -32,9 +38,100 @@ constexpr int kErrInvalidCode = static_cast(0x80412106); constexpr int kErrInvalidAddr = static_cast(0x80412107); constexpr int kErrNotConnected = static_cast(0x80412108); constexpr int kStateIpObtained = 3; +constexpr int kEventDisconnected = 1; +constexpr int kEventIpObtained = 3; constexpr std::uint8_t kFill = 0xa5; constexpr std::size_t kAddressLength = 16; +struct Delivery { + int calls = 0; + int event = 0; + void* arg = nullptr; +}; + +Delivery g_first; +Delivery g_second; +Delivery g_v6; +Delivery g_self; +int g_selfUnregister = -1; + +void APS5_VABI RecordFirst(int event, void* arg) { + ++g_first.calls; + g_first.event = event; + g_first.arg = arg; +} + +void APS5_VABI RecordSecond(int event, void* arg) { + ++g_second.calls; + g_second.event = event; + g_second.arg = arg; +} + +void APS5_VABI RecordV6(int event, void* arg) { + ++g_v6.calls; + g_v6.event = event; + g_v6.arg = arg; +} + +void APS5_VABI UnregisterSelf(int event, void* arg) { + ++g_self.calls; + g_self.event = event; + g_self.arg = arg; + g_selfUnregister = sceNetCtlUnregisterCallback(*static_cast(arg)); +} + +void CheckCallbacks() { + int state = -1; + Require(sceNetCtlGetState(&state) == 0); + const int event = state == kStateIpObtained ? kEventIpObtained : kEventDisconnected; + int firstProbe = 0; + int secondProbe = 0; + int v6Probe = 0; + + int v6 = -1; + Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); + + int first = -1; + Require(sceNetCtlRegisterCallback(RecordFirst, &firstProbe, &first) == 0 && first == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_first.event == event && g_first.arg == &firstProbe); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1); + + int second = -1; + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1); + Require(g_second.calls == 1 && g_second.event == event && g_second.arg == &secondProbe); + + Require(sceNetCtlUnregisterCallback(second) == 0); + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + Require(sceNetCtlUnregisterCallback(second) == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + + int self = -1; + Require(sceNetCtlRegisterCallback(UnregisterSelf, &self, &self) == 0 && self == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_self.calls == 1 && g_self.event == event && g_self.arg == &self && g_selfUnregister == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + Require(sceNetCtlCheckCallback() == 0); + Require(g_self.calls == 1); + + Require(sceNetCtlRegisterCallback(RecordSecond, &secondProbe, &second) == 0 && second == 1); + sceNetCtlTerm(); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 1 && g_second.calls == 1); + + Require(sceNetCtlRegisterCallbackV6(RecordV6, &v6Probe, &v6) == 0 && v6 == 0); + Require(sceNetCtlRegisterCallback(RecordFirst, &secondProbe, &first) == 0 && first == 0); + Require(sceNetCtlCheckCallback() == 0); + Require(g_first.calls == 2 && g_first.event == event && g_first.arg == &secondProbe); + Require(g_second.calls == 1 && g_self.calls == 1 && g_v6.calls == 0); + Require(sceNetCtlUnregisterCallback(first) == 0); + Require(sceNetCtlUnregisterCallbackV6(v6) == 0); +} + NetCtlInfo Query(int code) { NetCtlInfo info; std::memset(&info, kFill, sizeof(info)); @@ -90,6 +187,8 @@ int main() { WSADATA data{}; Require(WSAStartup(MAKEWORD(2, 2), &data) == 0); #endif + CheckCallbacks(); + NetCtlInfo info{}; Require(sceNetCtlGetInfo(0, &info) == kErrInvalidCode); Require(sceNetCtlGetInfo(22, &info) == kErrInvalidCode); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 3e0b95e6cb..464bcd0e32 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -124,6 +124,8 @@ static void Require(bool condition) { if (!condition) std::abort(); } +static void APS5_VABI IgnoreNetCtlEvent(int, void*) {} + static bool Failed(std::int64_t result, int error) { return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; } @@ -1428,20 +1430,19 @@ int main() { Require(second.tx_bytes >= first.tx_bytes && second.rx_bytes >= first.rx_bytes); } { - const auto dummyCallback = [](int, void*) {}; int cid = -1; Require(sceNetCtlRegisterCallbackV6(nullptr, nullptr, &cid) == static_cast(0x80412107)); - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, nullptr) == static_cast(0x80412107)); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, nullptr) == static_cast(0x80412107)); int cids[8] = {}; for (int i = 0; i < 8; ++i) { - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cids[i]) == 0); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cids[i]) == 0); Require(cids[i] == i); } - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cid) == static_cast(0x80412103)); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cid) == static_cast(0x80412103)); Require(sceNetCtlUnregisterCallbackV6(-1) == static_cast(0x80412105)); Require(sceNetCtlUnregisterCallbackV6(8) == static_cast(0x80412105)); Require(sceNetCtlUnregisterCallbackV6(3) == 0); - Require(sceNetCtlRegisterCallbackV6(dummyCallback, nullptr, &cid) == 0); + Require(sceNetCtlRegisterCallbackV6(IgnoreNetCtlEvent, nullptr, &cid) == 0); Require(cid == 3); for (int i = 0; i < 8; ++i) { Require(sceNetCtlUnregisterCallbackV6(i) == 0); diff --git a/docs/dev/debt/libSceNetCtl.md b/docs/dev/debt/libSceNetCtl.md index f7effa839a..519f8470dc 100644 --- a/docs/dev/debt/libSceNetCtl.md +++ b/docs/dev/debt/libSceNetCtl.md @@ -7,3 +7,7 @@ ### Unknown function info - [sceNetCtlGetInfo](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - answers from the host interface that holds an IPv4 address, preferring the one that carries the default route: its address, netmask, gateway, MTU and first two IPv4 DNS servers. The device is reported as wired, the link as connected, the IP configuration as DHCP and the HTTP proxy as off, whatever the host uses, and the Ethernet address is the one `sceNetGetMacAddress` reports. A gateway or DNS server the interface does not have is reported as `0.0.0.0`; what a console reports then is unknown. The DHCP host name (12), the PPPoE user name (13) and the HTTP proxy server (20) are empty strings, as that configuration sets none of them; the HTTP proxy port (21) is 0; the Wi-Fi codes (5-10) throw + +### Functional + +- [sceNetCtlCheckCallback](../../../core/libs/prx/libSceNetCtl/Export.cpp) (libSceNetCtl) - the state comes from the host interfaces, as in `sceNetCtlGetState`: `IPOBTAINED` (3) while the host has a non-loopback IPv4 or IPv6 address, `DISCONNECTED` (1) otherwise; the event values follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_codes.h#L27-L29), and no other event is delivered. Each callback registered with `sceNetCtlRegisterCallback` is called on the checking thread with its registration argument when the state differs from the last one it received, so a new registration receives the current state at the next check. This edge-triggered model follows [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/9e41d8320a2774b303fbad344c2347e8d0292e83/src/libs/network.cpp#L3648-L3661); [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/15abf68c9b52c4b04d094f2837719fdb871021f3/src/core/libraries/network/net_ctl_obj.cpp#L47-L57) delivers the state on every check instead, and neither is measured on hardware. KytyPS5 calls the callbacks under its lock; here they are called after the lock is released, from a copy of their slots, so a `sceNetCtlUnregisterCallback` or `sceNetCtlTerm` on another thread that returns while a check is in flight does not stop that check from calling the callback once more with its old argument. Callbacks registered with `sceNetCtlRegisterCallbackV6` keep their argument but receive no event, because no source says which call delivers them From 3518eb8deda36063e6c7d6195f0a8e22c1d22a69 Mon Sep 17 00:00:00 2001 From: Mehran Date: Thu, 15 Oct 2026 08:43:22 +0300 Subject: [PATCH 123/131] perf(agc): copy small draw snapshots into a per-recorder upload chunk --- .../Graphics/include/Recorder.hpp | 14 ++- .../Graphics/include/ShaderResources.hpp | 4 + .../prx/libSceAgcDriver/Graphics/src/Draw.cpp | 6 +- .../libSceAgcDriver/Graphics/src/Recorder.cpp | 18 ++- .../Graphics/src/ShaderResources.cpp | 45 +++++--- .../prx/libSceAgcDriver/tests/Recorder.cpp | 106 +++++++++++++++++- docs/dev/ENVIRONMENT.md | 1 + 7 files changed, 168 insertions(+), 26 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp index 30e547aaa6..f62ba12556 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp @@ -102,10 +102,17 @@ public: static constexpr std::size_t UnwrittenSnapshotEntries = 4096; static constexpr std::size_t DrawInputBudget = std::size_t{1024} << 20u; static constexpr std::size_t DrawInputEntries = 16384; - std::shared_ptr UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp); - void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer); + std::shared_ptr UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, VkDeviceSize* offset = nullptr); + void NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer, VkDeviceSize offset = 0); std::shared_ptr ReusableDrawSnapshot(std::uint64_t address, std::size_t bytes, SnapshotUse use = SnapshotUse::Storage, std::uint32_t* derived = nullptr, std::uint64_t generation = 0); void KeepDrawSnapshot(std::uint64_t address, std::size_t bytes, std::uint64_t generation, std::uint64_t registryGeneration, std::shared_ptr buffer, SnapshotUse use = SnapshotUse::Storage, std::uint32_t derived = 0); + static constexpr std::size_t DrawUploadChunk = std::size_t{1} << 20u; + struct UploadSlice { + std::shared_ptr buffer; + VkDeviceSize offset = 0; + std::byte* bytes = nullptr; + }; + UploadSlice DrawUpload(std::size_t bytes); static constexpr std::size_t IndexRangeEntries = 16384; static constexpr std::size_t KeptIndexRangeIndices = 4096; bool KnownHighestIndex(std::uint64_t address, std::size_t bytes, SnapshotUse use, std::optional& highest); @@ -812,6 +819,8 @@ private: std::uint64_t submissions = 0; std::size_t inFlightKeptBytes = 0; std::uint64_t writeNoteCount = 0; + std::shared_ptr drawUpload; + VkDeviceSize drawUploadUsed = 0; // Command buffers and fences of completed batches, reused by later ones (hundreds of batches per // frame would otherwise allocate and free their objects each time). std::vector> spare; @@ -836,6 +845,7 @@ private: struct UnwrittenSnapshot { std::size_t bytes; std::shared_ptr buffer; + VkDeviceSize offset = 0; }; std::unordered_map unwrittenSnapshots; std::array unwrittenStamp{}; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index a9a3d1d257..cf64092b8d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -165,6 +165,9 @@ public: struct Snapshot { std::uint64_t address; std::shared_ptr buffer; + VkDeviceSize offset = 0; + std::size_t bytes = 0; + std::span Bytes() const { return buffer->Bytes().subspan(static_cast(offset), bytes); } }; DescriptorCache* cache = nullptr; VkDescriptorSetLayout layout = VK_NULL_HANDLE; @@ -191,6 +194,7 @@ public: static const char* MoveRefusalName(MoveRefusal refusal); std::optional> MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const; static std::size_t MaxDrawSnapshotBytes(); + static std::size_t DrawUploadLimit(); void WriteBack(); // Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU // (storage images stay there; buffer ranges are noted so CPU reads wait); WriteBackBuffers runs diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index e7d0b91ff9..9aaa239642 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -1707,15 +1707,15 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c }; if (bindings != nullptr) { for (const auto& snapshot : bindings->snapshots) { - const auto bytes = snapshot.buffer->Bytes(); - addSample(snapshot.address, bytes.size(), snapshot.buffer->Handle(), 0, bytes.data()); + const auto bytes = snapshot.Bytes(); + addSample(snapshot.address, bytes.size(), snapshot.buffer->Handle(), snapshot.offset, bytes.data()); } } for (const auto& [begin, end] : resources.InPlaceReads()) { Require(end >= begin, "invalid capture input range"); const auto bytes = static_cast(end - begin); if (bytes == 0 || bytes > 512) continue; - if (bindings != nullptr && std::any_of(bindings->snapshots.begin(), bindings->snapshots.end(), [&](const auto& snapshot) { return snapshot.address < end && begin < snapshot.address + snapshot.buffer->Bytes().size(); })) continue; + if (bindings != nullptr && std::any_of(bindings->snapshots.begin(), bindings->snapshots.end(), [&](const auto& snapshot) { return snapshot.address < end && begin < snapshot.address + snapshot.bytes; })) continue; if (resources.WritesOverlap(begin, bytes) || recorder.PendingWriteOverlaps(begin, bytes)) { CaptureTrace::Log("input-skip draw=%llu batch=%llu address=%llx bytes=%zu reason=gpu-writer", draw, batch, static_cast(begin), bytes); continue; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp index aca8233093..43b90a9f1c 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp @@ -2542,6 +2542,17 @@ void Recorder::eraseDrawSnapshot(std::map::iterat drawSnapshots.erase(entry); } +Recorder::UploadSlice Recorder::DrawUpload(std::size_t bytes) { + const VkDeviceSize alignment = std::max(context.limits.minStorageBufferOffsetAlignment, 16); + auto offset = (drawUploadUsed + alignment - 1) & ~(alignment - 1); + if (drawUpload == nullptr || offset + bytes > drawUpload->Bytes().size()) { + drawUpload = std::make_shared(context, std::max(DrawUploadChunk, bytes), VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + offset = 0; + } + drawUploadUsed = offset + bytes; + return {drawUpload, offset, drawUpload->Bytes().data() + offset}; +} + void Recorder::retireUnwrittenSnapshots(const std::array& stamp) { if (stamp == unwrittenStamp) return; unwrittenSnapshots.clear(); @@ -2549,21 +2560,22 @@ void Recorder::retireUnwrittenSnapshots(const std::array& stam unwrittenStamp = stamp; } -std::shared_ptr Recorder::UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp) { +std::shared_ptr Recorder::UnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, VkDeviceSize* offset) { retireUnwrittenSnapshots(stamp); const auto found = unwrittenSnapshots.find(address); if (found == unwrittenSnapshots.end() || found->second.bytes != bytes) return {}; + if (offset != nullptr) *offset = found->second.offset; return found->second.buffer; } -void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer) { +void Recorder::NoteUnwrittenDrawSnapshot(std::uint64_t address, std::size_t bytes, const std::array& stamp, std::shared_ptr buffer, VkDeviceSize offset) { retireUnwrittenSnapshots(stamp); if (const auto found = unwrittenSnapshots.find(address); found != unwrittenSnapshots.end()) { unwrittenBytes -= found->second.bytes; unwrittenSnapshots.erase(found); } if (unwrittenSnapshots.size() >= UnwrittenSnapshotEntries || bytes > DrawSnapshotBudget - unwrittenBytes) return; - unwrittenSnapshots.emplace(address, UnwrittenSnapshot{bytes, std::move(buffer)}); + unwrittenSnapshots.emplace(address, UnwrittenSnapshot{bytes, std::move(buffer), offset}); unwrittenBytes += bytes; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d02104e8e7..15463777df 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -4950,10 +4950,19 @@ std::size_t ShaderResources::MaxDrawSnapshotBytes() { return static_cast(mib * 1024ull * 1024ull); } +std::size_t ShaderResources::DrawUploadLimit() { + static const std::size_t limit = [] { + const char* value = std::getenv("APS5_DRAW_UPLOAD_MAX"); + return value != nullptr ? static_cast(std::strtoull(value, nullptr, 10)) : std::size_t{4096}; + }(); + return limit; +} + std::shared_ptr ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span moved, bool movedOnly) const { if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {}; const auto reads = guestMemory.InPlaceReads(); const auto maxSnapshotBytes = maxDrawSnapshotBytes; + const auto uploadLimit = DrawUploadLimit(); std::size_t totalSnapshotBytes = 0; auto result = std::make_shared(); thread_local std::vector* selectedSlot = nullptr; @@ -4981,7 +4990,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi else movedWordBuffers[movedWordBufferNext++ % MovedWordBufferSlots] = std::move(entry); } selected.push_back(index); - result->snapshots.push_back({0, std::move(buffer)}); + result->snapshots.push_back({0, std::move(buffer), 0, override->size}); continue; } std::uint64_t address = item.address; @@ -5002,28 +5011,38 @@ std::shared_ptr ShaderResources::PrepareDrawBindi stamp = {GuestMemory::TrackerGeneration(), GuestMemory::CollectEpoch(), registryGeneration}; stamped = true; } - auto buffer = recorder.UnwrittenDrawSnapshot(begin, bytes, stamp); + VkDeviceSize offset = 0; + auto buffer = recorder.UnwrittenDrawSnapshot(begin, bytes, stamp, &offset); static const bool verifySnapshots = std::getenv("APS5_VERIFY_DRAW_SNAPSHOTS") != nullptr; if (verifySnapshots && buffer != nullptr) { static std::atomic checked{0}, differing{0}; + const auto fast = buffer->Bytes().subspan(static_cast(offset), bytes); const auto slow = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, GuestMemory::CollectWrites(begin, bytes)); - const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == buffer->Bytes().size() && std::memcmp(slow->Bytes().data(), buffer->Bytes().data(), buffer->Bytes().size()) == 0) || (slow == nullptr && std::memcmp(buffer->Bytes().data(), reinterpret_cast(begin), bytes) == 0); + const bool same = slow == buffer || (slow != nullptr && slow->Bytes().size() == fast.size() && std::memcmp(slow->Bytes().data(), fast.data(), fast.size()) == 0) || (slow == nullptr && std::memcmp(fast.data(), reinterpret_cast(begin), bytes) == 0); if (!same) differing.fetch_add(1); if (checked.fetch_add(1) % 200000 == 199999) std::fprintf(stderr, "[draw-snapshots] verify: %llu fast answers checked, %llu differing%c", static_cast(checked.load()), static_cast(differing.load()), 10); } if (buffer == nullptr) { - const auto generation = GuestMemory::CollectWrites(begin, bytes); - buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); - if (buffer == nullptr) { - buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); - std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); - recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer); + const bool uploaded = bytes != 0 && bytes <= uploadLimit; + if (uploaded) { + auto slice = recorder.DrawUpload(bytes); + std::memcpy(slice.bytes, reinterpret_cast(begin), bytes); + buffer = std::move(slice.buffer); + offset = slice.offset; + } else { + const auto generation = GuestMemory::CollectWrites(begin, bytes); + buffer = recorder.ReusableDrawSnapshot(begin, bytes, Recorder::SnapshotUse::Storage, nullptr, generation); + if (buffer == nullptr) { + buffer = std::make_shared(context, bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT); + std::memcpy(buffer->Bytes().data(), reinterpret_cast(begin), bytes); + recorder.KeepDrawSnapshot(begin, bytes, generation, registryGeneration, buffer); + } } - if (GuestMemory::TrackerGeneration() == stamp[0]) recorder.NoteUnwrittenDrawSnapshot(begin, bytes, stamp, buffer); - else stamped = false; + if (GuestMemory::TrackerGeneration() != stamp[0]) stamped = false; + else if (!uploaded || stamp[1] != 0) recorder.NoteUnwrittenDrawSnapshot(begin, bytes, stamp, buffer, offset); } selected.push_back(index); - result->snapshots.push_back({begin, std::move(buffer)}); + result->snapshots.push_back({begin, std::move(buffer), offset, bytes}); totalSnapshotBytes += bytes; CaptureTrace::Log("draw-snapshot batch=%llu address=%llx bytes=%zu", static_cast(recorder.Submissions() + 1), static_cast(begin), bytes); } @@ -5072,7 +5091,7 @@ std::shared_ptr ShaderResources::PrepareDrawBindi const auto snapshotIndex = snapshotIndices[binding.allocations[element]]; if (snapshotIndex == selected.size()) continue; const auto& snapshot = result->snapshots[snapshotIndex]; - buffers[offset + element] = {snapshot.buffer->Handle(), 0, snapshot.buffer->Bytes().size()}; + buffers[offset + element] = {snapshot.buffer->Handle(), snapshot.offset, snapshot.bytes}; } } } diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 2e9cd49f46..ec87d09524 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -2531,8 +2531,9 @@ void resourceReadTests(const Device& device, Recorder& recorder) { const auto commands = snapshotRecorder.Commands(); RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_HOST_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT); const auto copy = snapshotContext.Function("vkCmdCopyBuffer"); - VkBufferCopy region{0, 0, elementBytes}; + VkBufferCopy region{first->snapshots[0].offset, 0, elementBytes}; copy(commands, first->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion); + region.srcOffset = second->snapshots[0].offset; region.dstOffset = elementBytes; copy(commands, second->snapshots[0].buffer->Handle(), downloaded->Handle(), 1, ®ion); RecordMemoryBarrier(snapshotContext, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT); @@ -2878,7 +2879,7 @@ void misalignedRegionTests(const Device& device, Recorder& recorder) { const auto check = [&] { const auto bindings = resources.PrepareDrawBindings(regionRecorder); Require(bindings != nullptr && bindings->snapshots.size() == 1, "the read-only misaligned draw input was not snapshotted"); - const auto contents = bindings->snapshots.front().buffer->Bytes(); + const auto contents = bindings->snapshots.front().Bytes(); Require(bindings->snapshots.front().address == view - adjustment && contents.size() == adjustment + elementBytes, "the draw snapshot does not start at the aligned offset below the view"); Require(std::memcmp(contents.data() + adjustment, guest + offset, elementBytes) == 0, "the shader's patched offset into the draw snapshot misses the view's bytes"); }; @@ -2963,9 +2964,9 @@ void misalignedSnapshotTests(const Device& device, Recorder& recorder) { Require(push[0] == std::byte{0} && adjustment == offset % alignment, "the inner view's shader offset is not its distance from the binding"); const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); Require(bindings != nullptr && bindings->snapshots.size() == 2, "read-only draw inputs were not snapshotted"); - const auto outerContents = bindings->snapshots[0].buffer->Bytes(); + const auto outerContents = bindings->snapshots[0].Bytes(); Require(outerContents.size() >= outerBytes && std::memcmp(outerContents.data(), guest + outer, outerBytes) == 0, "an aligned draw snapshot misses its view's bytes"); - const auto contents = bindings->snapshots[1].buffer->Bytes(); + const auto contents = bindings->snapshots[1].Bytes(); Require(contents.size() >= adjustment + elementBytes, "a draw snapshot ends before the bytes the shader reads"); Require(std::memcmp(contents.data() + adjustment, guest + offset, elementBytes) == 0, "the shader's offset into a draw snapshot misses the view's bytes"); snapshotRecorder.Sync(); @@ -3185,7 +3186,7 @@ void drawSnapshotReuseTests(const Device& device, Recorder& recorder) { return; } const auto element = address + 4096; - constexpr std::size_t elementBytes = 1024; + constexpr std::size_t elementBytes = 8192; ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; @@ -3635,6 +3636,99 @@ void unwrittenSnapshotTests(const Device& device) { for (std::size_t entry = 0; entry < Recorder::UnwrittenSnapshotEntries; ++entry) cache.NoteUnwrittenDrawSnapshot(address + entry * 16, 1, next, buffer); cache.NoteUnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next, buffer); Require(cache.UnwrittenDrawSnapshot(address, 1, next) == buffer && cache.UnwrittenDrawSnapshot(address + Recorder::UnwrittenSnapshotEntries * 16, 1, next) == nullptr, "unwritten snapshots grew past the entry cap"); + const std::array later{1, 3, 1}; + cache.NoteUnwrittenDrawSnapshot(address, 16, later, buffer, 256); + VkDeviceSize offset = 0; + Require(cache.UnwrittenDrawSnapshot(address, 16, later, &offset) == buffer && offset == 256, "an unwritten snapshot lost its offset in its buffer"); +} + +void drawSnapshotUploadTests(const Device& device, Recorder& recorder) { + const auto& context = device.GetContext(); + const auto limit = ShaderResources::DrawUploadLimit(); + if (limit < 256 || limit > 16384) { + std::cout << "draw upload limit outside 256..16384 bytes: draw snapshot uploads not tested\n"; + return; + } + constexpr std::size_t bytes = 65536; + void* block = context.hostImportAlignment != 0 ? AllocateWatched(bytes, 65536) : nullptr; + if (block == nullptr) { + std::cout << "host imports or write watching unavailable: draw snapshot uploads not tested\n"; + return; + } + struct Release { + void* block; + ~Release() { ReleaseWatched(block, bytes); } + } release{block}; + for (std::size_t index = 0; index < bytes; ++index) static_cast(block)[index] = static_cast(index * 7u + 1u); + const auto address = reinterpret_cast(block); + { + GuestAllocations::Mutation mutation; + mutation.Add(block, bytes, true, true, true); + } + struct Unregister { + const Context& context; + void* block; + ~Unregister() { + { + GuestAllocations::Mutation mutation; + mutation.Remove(block); + } + HostImportFor(context, reinterpret_cast(block), bytes); + } + } unregister{context, block}; + if (HostImportFor(context, address, bytes) == nullptr) { + std::cout << "host import of the watched block refused: draw snapshot uploads not tested\n"; + return; + } + const std::array elements{address + 4096, address + 8192, address + 32768}; + const std::array sizes{256, limit, limit + 256}; + ShaderRecompiler::RecompileResult program; + for (std::uint32_t index = 0; index < elements.size(); ++index) { + ShaderRecompiler::DescriptorBinding binding; + binding.kind = ShaderRecompiler::DescriptorKind::StorageBuffer; + binding.role = ShaderRecompiler::DescriptorRole::GuestBuffers; + binding.descriptorSet = 0; + binding.binding = index; + binding.count = 1; + binding.guestDescriptor = {static_cast(elements[index]), static_cast(elements[index] >> 32u) & 0xffffu, static_cast(sizes[index]), 0x31000000u}; + binding.bufferWritten = {false}; + program.bindings.push_back(binding); + } + const CompiledShader compute{ShaderRecompiler::ShaderStage::Compute, &program, 0}; + { + auto snapshotContext = context; + DescriptorCache cache(snapshotContext); + snapshotContext.descriptorCache = &cache; + Recorder snapshotRecorder(snapshotContext); + snapshotRecorder.Activate(); + ShaderResources resources(snapshotContext, compute); + const auto alignment = std::max(snapshotContext.limits.minStorageBufferOffsetAlignment, 16); + const auto bindings = resources.PrepareDrawBindings(snapshotRecorder); + Require(bindings != nullptr && bindings->snapshots.size() == elements.size(), "read-only draw inputs were not snapshotted"); + for (std::size_t index = 0; index < elements.size(); ++index) { + const auto& snapshot = bindings->snapshots[index]; + Require(snapshot.address == elements[index] && snapshot.bytes == sizes[index] && snapshot.offset % alignment == 0, "a draw snapshot is not an aligned copy of its input"); + Require(std::memcmp(snapshot.Bytes().data(), reinterpret_cast(elements[index]), sizes[index]) == 0, "a draw snapshot does not hold the guest bytes of its draw"); + } + Require(bindings->snapshots[0].buffer == bindings->snapshots[1].buffer && bindings->snapshots[0].offset != bindings->snapshots[1].offset, "draw inputs up to the upload limit do not share the recorder's upload chunk"); + Require(bindings->snapshots[2].buffer != bindings->snapshots[0].buffer && bindings->snapshots[2].offset == 0, "a draw input above the upload limit was copied into the upload chunk"); + const auto earlier = bindings->snapshots[0].Bytes(); + const std::vector before(earlier.begin(), earlier.end()); + std::memset(reinterpret_cast(elements[0]), 0x5a, sizes[0]); + const auto again = resources.PrepareDrawBindings(snapshotRecorder); + Require(again != nullptr && again->snapshots.size() == elements.size(), "read-only draw inputs were not snapshotted again"); + const auto copied = again->snapshots[0].Bytes(); + Require(again->snapshots[0].buffer != bindings->snapshots[0].buffer || again->snapshots[0].offset != bindings->snapshots[0].offset, "a draw input copied again took the slice an earlier draw reads"); + Require(std::all_of(copied.begin(), copied.end(), [](std::byte value) { return value == std::byte{0x5a}; }), "a draw snapshot copied again misses the CPU store"); + Require(std::equal(before.begin(), before.end(), earlier.begin()), "copying a draw input again changed the slice an earlier draw reads"); + const auto first = snapshotRecorder.DrawUpload(16); + const auto second = snapshotRecorder.DrawUpload(16); + Require(second.buffer == first.buffer && second.offset >= first.offset + 16 && second.offset % alignment == 0, "upload slices overlap or are not aligned"); + const auto whole = snapshotRecorder.DrawUpload(Recorder::DrawUploadChunk); + Require(whole.buffer != second.buffer && whole.offset == 0, "a full upload chunk was not replaced"); + snapshotRecorder.Sync(); + } + recorder.Activate(); } void drawInputReuseTests(const Device& device, Recorder& recorder) { @@ -9022,6 +9116,7 @@ int main(int argc, char** argv) { if (argc == 2 && std::string_view(argv[1]) == "--draw-snapshot-cap-only") { drawSnapshotCapTests(device, recorder); unwrittenSnapshotTests(device); + drawSnapshotUploadTests(device, recorder); std::cout << "Draw snapshot cap tests passed\n"; return 0; } @@ -9071,6 +9166,7 @@ int main(int argc, char** argv) { drawSnapshotCapTests(device, recorder); drawSnapshotEvictionTests(device); unwrittenSnapshotTests(device); + drawSnapshotUploadTests(device, recorder); drawInputReuseTests(device, recorder); drawInputInPlaceTests(device, recorder); RunResidentPresentTests(device.GetContext()); diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 70584350e4..bf77cac30d 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -115,6 +115,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | `APS5_DRAW_KEY_ALL_USER_WORDS` | presence | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp` | | includes all 32 user words in a draw's register key instead of the used count | | `APS5_DRAW_SNAPSHOT_MAX_MIB` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 64 | upper bound in MiB on the snapshot bytes a draw's bindings may take | | `APS5_DRAW_TRANSITIONS` | presence | `core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp` | | keeps the per-draw upload and download barriers and layout transitions instead of the lean recorded path | +| `APS5_DRAW_UPLOAD_MAX` | integer | `core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp` | 4096 | largest draw snapshot in bytes copied into the per-recorder upload chunk; 0 turns the chunk off | | `APS5_DUMP_COMPUTE_MIN_WORDS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | only captures compute pipelines whose SPIR-V has at least this many words | | `APS5_DUMP_COMPUTE_PIPELINES` | string | `core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp` | | directory in which captured compute pipelines are written | | `APS5_DUMP_DRAW_SHADERS` | integer | `core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp` | 0 | hex color-target or slot-0 address whose draw's shaders are dumped | From 8ff509a9ab5d9cf156342747d15b9d5e240eff98 Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sun, 18 Oct 2026 19:03:15 +0300 Subject: [PATCH 124/131] fix(agc): honor GPU selection in overlay tests (cherry picked from commit 932eeb085e8e7db2eff4615d9a24786001dc7ad7) --- core/libs/prx/libSceAgcDriver/CMakeLists.txt | 7 +- .../tests/PerformanceOverlay.cpp | 94 ++++++++++++++++++- 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6bf99b3a18..7fcc4ee3eb 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -1466,9 +1466,12 @@ if(BUILD_TESTING) set_target_properties(agc_performance_overlay_statistics_tests agc_performance_overlay_tests PROPERTIES EXCLUDE_FROM_ALL OFF) add_test(NAME agc_performance_overlay_statistics COMMAND agc_performance_overlay_statistics_tests) add_test(NAME agc_performance_overlay COMMAND agc_performance_overlay_tests) - set_tests_properties(agc_performance_overlay PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + add_test(NAME agc_performance_overlay_device_selection COMMAND agc_performance_overlay_tests device-selection) + add_test(NAME agc_performance_overlay_device_selection_cpu COMMAND agc_performance_overlay_tests device-selection-cpu) + set_tests_properties(agc_performance_overlay agc_performance_overlay_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30) + set_tests_properties(agc_performance_overlay_device_selection_cpu PROPERTIES TIMEOUT 30) if(WIN32) - set_tests_properties(agc_performance_overlay_statistics agc_performance_overlay PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(agc_performance_overlay_statistics agc_performance_overlay agc_performance_overlay_device_selection agc_performance_overlay_device_selection_cpu PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp index 860f34e2c3..9b5b6531f2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/PerformanceOverlay.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -13,7 +14,10 @@ #include #include #include +#include +#include #include +#include #include namespace { @@ -29,6 +33,67 @@ using AgcDriver::PresentationPass; constexpr int VulkanTestSkipped = 77; constexpr std::uint64_t Bgra8 = 0x8000000000000000ull; +std::optional selectDevice(std::span devices, std::string_view request) { + const auto rankDeviceType = [](VkPhysicalDeviceType type) { + switch (type) { + case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3; + case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2; + case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1; + default: return 0; + } + }; + std::optional selected; + int selectedRank = -1; + for (std::size_t index = 0; index < devices.size(); ++index) { + const auto& candidate = devices[index]; + if (candidate.apiVersion < VK_API_VERSION_1_1) continue; + const std::string_view name(candidate.deviceName); + if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) { + return std::tolower(static_cast(left)) == std::tolower(static_cast(right)); + }) == name.end()) continue; + const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4; + if (rank <= selectedRank) continue; + selected = index; + selectedRank = rank; + } + return selected; +} + +void deviceSelectionTests() { + const auto properties = [](const char* name, VkPhysicalDeviceType type) { + VkPhysicalDeviceProperties result{}; + result.apiVersion = VK_API_VERSION_1_1; + result.deviceType = type; + std::memcpy(result.deviceName, name, std::strlen(name) + 1u); + return result; + }; + std::array devices{ + properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU), + properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU), + properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU), + properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU), + }; + Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device"); + Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU"); + Require(selectDevice(devices, "AMD") == 0u, "the AMD request did not select the integrated GPU"); + Require(selectDevice(devices, "NVIDIA") == 1u, "the NVIDIA request did not select the discrete GPU"); + Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU"); + Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case"); + Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected"); + Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected"); + Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU"); + Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched"); + Require(selectDevice(std::span(devices).subspan(2), {}) == 0u, "default selection did not prefer the virtual GPU to the CPU"); + devices[1].apiVersion = VK_API_VERSION_1_0; + Require(selectDevice(devices, {}) == 0u, "default selection did not prefer the integrated GPU to the virtual GPU"); + Require(!selectDevice(devices, "NVIDIA").has_value(), "a requested device without Vulkan 1.1 was selected"); + devices[1] = devices[0]; + Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "AMD") == 0u, "equally ranked matches did not retain enumeration order"); + for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0; + Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device"); + std::puts("performance-overlay device selection CPU tests passed"); +} + class Device { public: Device() { @@ -52,7 +117,13 @@ public: Require(count != 0, "no Vulkan device"); std::vector devices(count); Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices"); - context.physical = devices.front(); + const auto physicalProperties = function("vkGetPhysicalDeviceProperties"); + std::vector deviceProperties(devices.size()); + for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]); + const char* requestedName = std::getenv("ANYPS5_GPU"); + const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : ""); + Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device"); + context.physical = devices[*selectedIndex]; const auto queues = function("vkGetPhysicalDeviceQueueFamilyProperties"); queues(context.physical, &count, nullptr); std::vector families(count); @@ -104,6 +175,14 @@ public: std::uint32_t TimestampBits() const { return timestampBits; } bool MemoryBudget() const { return memoryBudget; } + void VerifySelection() const { + VkPhysicalDeviceProperties properties{}; + function("vkGetPhysicalDeviceProperties")(context.physical, &properties); + std::cout << "Selected GPU name=" << properties.deviceName << " vendor=" << properties.vendorID << " device=" << properties.deviceID << '\n'; + const char* request = std::getenv("ANYPS5_GPU"); + Require(selectDevice(std::span(&properties, 1), request != nullptr ? request : "").has_value(), "the performance-overlay fixture physical device does not match ANYPS5_GPU"); + } + private: template TFunction function(const char* functionName) const { @@ -604,18 +683,27 @@ void costReport(const Device& device, PerformanceOverlay& overlay) { } -int main() { +int main(int argc, char** argv) { try { + const std::string_view mode = argc > 1 ? argv[1] : ""; + Require(argc <= 2 && (mode.empty() || mode == "device-selection" || mode == "device-selection-cpu"), "unknown performance-overlay test mode"); + if (mode == "device-selection-cpu") { + deviceSelectionTests(); + return 0; + } setEnvironment("ANYPS5_OVERLAY", ""); setEnvironment("ANYPS5_OVERLAY_POS", ""); std::unique_ptr device; try { device = std::make_unique(); } catch (const std::exception& error) { - if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw; + const char* requestedName = std::getenv("ANYPS5_GPU"); + if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || (requestedName != nullptr && *requestedName != '\0')) throw; std::printf("skipped, no usable Vulkan device: %s\n", error.what()); return VulkanTestSkipped; } + device->VerifySelection(); + if (mode == "device-selection") return 0; const auto& context = device->GetContext(); { PerformanceOverlay overlay(context, 1); From c33cf11f0639f03091066cb7377967dbd5b5fafa Mon Sep 17 00:00:00 2001 From: BIGFERRY999 Date: Thu, 15 Oct 2026 00:54:04 +0100 Subject: [PATCH 125/131] feat(libc): implement fdim and fdimf --- core/libs/prx/libc/src/Math.cpp | 2 ++ core/libs/tests/GuestMath.cpp | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index ddeea98f8c..40587106b1 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -319,6 +319,8 @@ long double APS5_VABI fminl_nid_postfix(long double x, long double y) { return G double APS5_VABI fmax_nid_postfix(double x, double y) { return GuestFmax(x, y); } float APS5_VABI fmaxf_nid_postfix(float x, float y) { return GuestFmax(x, y); } long double APS5_VABI fmaxl_nid_postfix(long double x, long double y) { return GuestFmax(x, y); } +double APS5_VABI fdim_nid_postfix(double x, double y) { return std::fdim(x, y); } +float APS5_VABI fdimf_nid_postfix(float x, float y) { return std::fdim(x, y); } long double APS5_VABI acosl_nid_postfix(long double x) { return std::acos(x); } long double APS5_VABI acoshl_nid_postfix(long double x) { return std::acosh(x); } long double APS5_VABI asinhl_nid_postfix(long double x) { return std::asinh(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index d095eda0ff..5f15ebd7c2 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -69,6 +69,8 @@ double APS5_VABI asin_nid_postfix(double); float APS5_VABI atan2f_nid_postfix(float, 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); +float APS5_VABI fdimf_nid_postfix(float, float); float APS5_VABI tanf_nid_postfix(float); int APS5_VABI rand_nid_postfix(); void APS5_VABI srand_nid_postfix(unsigned int); @@ -210,6 +212,13 @@ static void CheckExactOperations() { Require(fma_nid_postfix(tenth, 10., -1.) == 0x1p-54); Require(fma_nid_postfix(0x1p1023, 2., -0x1p1023) == 0x1p1023); Require(std::isnan(fma_nid_postfix(INFINITY, 0., 1.))); + Require(fdim_nid_postfix(4.0, 1.0) == 3.0 && fdim_nid_postfix(1.0, 4.0) == 0.0); + Require(fdim_nid_postfix(4.0, 4.0) == 0.0 && !std::signbit(fdim_nid_postfix(1.0, 4.0))); + Require(std::isnan(fdim_nid_postfix(std::numeric_limits::quiet_NaN(), 1.0))); + Require(std::isnan(fdim_nid_postfix(1.0, std::numeric_limits::quiet_NaN()))); + Require(fdimf_nid_postfix(4.0f, 1.0f) == 3.0f && fdimf_nid_postfix(1.0f, 4.0f) == 0.0f); + Require(fdimf_nid_postfix(4.0f, 4.0f) == 0.0f && !std::signbit(fdimf_nid_postfix(1.0f, 4.0f))); + Require(std::isnan(fdimf_nid_postfix(std::numeric_limits::quiet_NaN(), 1.0f))); } static void CheckNearbyint() { From 19b8fb32398424aedd4462df7bea237403ef24d1 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:49:20 +0200 Subject: [PATCH 126/131] fix(libc): bound the stack dump by the thread's stack top and keep its buffers off the stack --- core/libs/prx/libc/CMakeLists.txt | 5 +++++ core/libs/prx/libc/src/specifics/windows/CrashReport.cpp | 9 +++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index deee94494f..ce0de0bce5 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -841,6 +841,11 @@ if(MINGW) set_tests_properties(windows_crash_stack_words PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=set:2048" PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005[^~]*stack words from rsp[^~]*data at stack words that point outside the stack[^~]*c0ffee0000000000 c0ffee0000000001 c0ffee0000000002 c0ffee0000000003 c0ffee0000000004 c0ffee0000000005 c0ffee0000000006 c0ffee0000000007") + add_test(NAME windows_crash_stack_words_bounded COMMAND windows_backtrace_report_tests fault) + set_tests_properties(windows_crash_stack_words_bounded PROPERTIES TIMEOUT 20 + ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=set:0x100000" + PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005[^~]*stack words from rsp" + FAIL_REGULAR_EXPRESSION " [+]0x[1-9a-f][0-9a-f][0-9a-f][0-9a-f]") add_test(NAME windows_crash_without_stack_words COMMAND windows_backtrace_report_tests fault) set_tests_properties(windows_crash_without_stack_words PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=unset:" diff --git a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp index e7c1694e1a..c23d288aa9 100644 --- a/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp +++ b/core/libs/prx/libc/src/specifics/windows/CrashReport.cpp @@ -271,9 +271,10 @@ void ReportStackWords(std::uint64_t rsp) { const auto stackHigh = reinterpret_cast(tib->StackBase); const auto first = rsp & ~std::uint64_t{7}; Report(" stack words from rsp 0x%016llx (APS5_CRASH_STACK_BYTES):\n", static_cast(rsp)); + const auto limit = std::min(first + bytes, stackHigh); std::uint64_t end = first; std::uint64_t readablePage = 0; - for (std::uint64_t at = first; at < first + bytes; at += 32) { + for (std::uint64_t at = first; at < limit; at += 32) { std::uint64_t words[4] = {}; int count = 0; for (; count < 4; ++count) { @@ -286,14 +287,14 @@ void ReportStackWords(std::uint64_t rsp) { words[count] = *reinterpret_cast(slot); } if (count == 0) break; - char text[192]; + static char text[192]; int used = std::snprintf(text, sizeof(text), " +0x%04llx:", static_cast(at - first)); for (int k = 0; k < count && used > 0 && used < static_cast(sizeof(text)); ++k) used += std::snprintf(text + used, sizeof(text) - static_cast(used), " %016llx", static_cast(words[k])); Report("%s\n", text); end = at + static_cast(count) * 8; if (count < 4) break; } - std::uint64_t seen[256]; + static std::uint64_t seen[256]; std::size_t seenCount = 0; Report(" data at stack words that point outside the stack (8 words each):\n"); for (std::uint64_t slot = first; slot + 8 <= end && seenCount < std::size(seen); slot += 8) { @@ -429,7 +430,7 @@ LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) { } ReportStackCandidates(context->Rsp, 16, 0x2000); ReportStackWords(context->Rsp); - if (context->Rip == 0|| !IsExecutable(context->Rip)) { + if (context->Rip == 0 ||!IsExecutable(context->Rip)) { // A jump into nothing usually follows a return through a clobbered frame; the frames that just // returned are still below rsp. Report(" recently popped return address candidates:\n"); From 0edfefaf18ebf3b49bb6fce00b217e89aa1c0fa4 Mon Sep 17 00:00:00 2001 From: BIGFERRY999 Date: Thu, 15 Oct 2026 00:51:38 +0100 Subject: [PATCH 127/131] feat(libc): implement ilogb and ilogbf --- core/libs/prx/libc/src/Math.cpp | 2 ++ core/libs/tests/GuestMath.cpp | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index ddeea98f8c..f7f161c58e 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -545,6 +545,8 @@ long double APS5_VABI powl_nid_postfix(long double base, long double exp) { retu long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::ldexp(x, exp); } long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); } double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); } +int APS5_VABI ilogb_nid_postfix(double x) { return std::ilogb(x); } +int APS5_VABI ilogbf_nid_postfix(float x) { return std::ilogb(x); } double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index d095eda0ff..e72d8df55f 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -83,6 +83,8 @@ float APS5_VABI erff_nid_postfix(float); double APS5_VABI lgamma_nid_postfix(double); extern int signgam_nid_postfix; float APS5_VABI logbf_nid_postfix(float); +int APS5_VABI ilogb_nid_postfix(double); +int APS5_VABI ilogbf_nid_postfix(float); double APS5_VABI exp2_nid_postfix(double); float APS5_VABI __powisf2_nid_postfix(float, int); double APS5_VABI __powidf2_nid_postfix(double, int); @@ -1053,6 +1055,10 @@ int main() { Require(logbf_nid_postfix(0.f) == -std::numeric_limits::infinity()); Require(logbf_nid_postfix(-std::numeric_limits::infinity()) == std::numeric_limits::infinity()); Require(std::isnan(logbf_nid_postfix(std::numeric_limits::quiet_NaN()))); + Require(ilogb_nid_postfix(8.0) == 3 && ilogb_nid_postfix(-0.75) == -1 && ilogb_nid_postfix(1.0) == 0); + Require(ilogb_nid_postfix(1024.0) == 10 && ilogb_nid_postfix(0.125) == -3); + Require(ilogbf_nid_postfix(8.f) == 3 && ilogbf_nid_postfix(-0.75f) == -1 && ilogbf_nid_postfix(1.f) == 0); + Require(ilogbf_nid_postfix(1024.f) == 10 && ilogbf_nid_postfix(0.125f) == -3); Require(exp2_nid_postfix(-3.) == 0.125); Require(ldexp_nid_postfix(0.75, 4) == 12.); CheckSquareRoot(); From 3b5c41b20f7dccff0d6bc82612657e360c99fb2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Sun, 18 Oct 2026 18:49:34 +0200 Subject: [PATCH 128/131] fix(libc): ilogb special cases match PS5 libc.prx (0 -> INT_MIN, inf/NaN -> INT_MAX) Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/src/Math.cpp | 13 +++++++++++-- core/libs/tests/GuestMath.cpp | 9 +++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index f7f161c58e..8e3d9be003 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -545,8 +546,16 @@ long double APS5_VABI powl_nid_postfix(long double base, long double exp) { retu long double APS5_VABI ldexpl_nid_postfix(long double x, int exp) { return std::ldexp(x, exp); } long double APS5_VABI truncl_nid_postfix(long double x) { return std::trunc(x); } double APS5_VABI logb_nid_postfix(double x) { return std::logb(x); } -int APS5_VABI ilogb_nid_postfix(double x) { return std::ilogb(x); } -int APS5_VABI ilogbf_nid_postfix(float x) { return std::ilogb(x); } +int APS5_VABI ilogb_nid_postfix(double x) { + if (x == 0.0) { + return INT_MIN; + } + if (!std::isfinite(x)) { + return INT_MAX; + } + return std::ilogb(x); +} +int APS5_VABI ilogbf_nid_postfix(float x) { return ilogb_nid_postfix(static_cast(x)); } double APS5_VABI remainder_nid_postfix(double x, double y) { return std::remainder(x, y); } float APS5_VABI _FSinh_nid_postfix(float x, float y) { return y * std::sinh(x); } float APS5_VABI _FCosh_nid_postfix(float x, float y) { return y * std::cosh(x); } diff --git a/core/libs/tests/GuestMath.cpp b/core/libs/tests/GuestMath.cpp index e72d8df55f..ba97c801a6 100644 --- a/core/libs/tests/GuestMath.cpp +++ b/core/libs/tests/GuestMath.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -1059,6 +1060,14 @@ int main() { Require(ilogb_nid_postfix(1024.0) == 10 && ilogb_nid_postfix(0.125) == -3); Require(ilogbf_nid_postfix(8.f) == 3 && ilogbf_nid_postfix(-0.75f) == -1 && ilogbf_nid_postfix(1.f) == 0); Require(ilogbf_nid_postfix(1024.f) == 10 && ilogbf_nid_postfix(0.125f) == -3); + Require(ilogb_nid_postfix(0.0) == INT_MIN && ilogb_nid_postfix(-0.0) == INT_MIN); + Require(ilogb_nid_postfix(std::numeric_limits::infinity()) == INT_MAX); + Require(ilogb_nid_postfix(-std::numeric_limits::infinity()) == INT_MAX); + Require(ilogb_nid_postfix(std::numeric_limits::quiet_NaN()) == INT_MAX); + Require(ilogbf_nid_postfix(0.f) == INT_MIN && ilogbf_nid_postfix(-0.f) == INT_MIN); + Require(ilogbf_nid_postfix(std::numeric_limits::infinity()) == INT_MAX); + Require(ilogbf_nid_postfix(std::numeric_limits::quiet_NaN()) == INT_MAX); + Require(ilogbf_nid_postfix(std::numeric_limits::denorm_min()) == -149); Require(exp2_nid_postfix(-3.) == 0.125); Require(ldexp_nid_postfix(0.75, 4) == 12.); CheckSquareRoot(); From 78869e5a47464aa4b0bab8dc95e50807c165a88d Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:57:49 +0200 Subject: [PATCH 129/131] test(libc): drop the stack bound case, it also passes without the bound --- core/libs/prx/libc/CMakeLists.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ce0de0bce5..deee94494f 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -841,11 +841,6 @@ if(MINGW) set_tests_properties(windows_crash_stack_words PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=set:2048" PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005[^~]*stack words from rsp[^~]*data at stack words that point outside the stack[^~]*c0ffee0000000000 c0ffee0000000001 c0ffee0000000002 c0ffee0000000003 c0ffee0000000004 c0ffee0000000005 c0ffee0000000006 c0ffee0000000007") - add_test(NAME windows_crash_stack_words_bounded COMMAND windows_backtrace_report_tests fault) - set_tests_properties(windows_crash_stack_words_bounded PROPERTIES TIMEOUT 20 - ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=set:0x100000" - PASS_REGULAR_EXPRESSION "FATAL: unhandled exception 0xc0000005[^~]*stack words from rsp" - FAIL_REGULAR_EXPRESSION " [+]0x[1-9a-f][0-9a-f][0-9a-f][0-9a-f]") add_test(NAME windows_crash_without_stack_words COMMAND windows_backtrace_report_tests fault) set_tests_properties(windows_crash_without_stack_words PROPERTIES TIMEOUT 20 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$;APS5_CRASH_STACK_BYTES=unset:" From 40bc17cb862660728237843b3f32805d32e6b71b Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 17:04:35 +0000 Subject: [PATCH 130/131] fix(libkernel): return errno when Windows CPU clock queries fail --- core/libs/prx/libkernel/Time/Time.cpp | 38 +++++++++++++------ core/libs/prx/libkernel/Time/include/Time.hpp | 3 ++ core/libs/tests/GuestCpuTimeClocks.cpp | 14 +++++++ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 12523a767b..5169aebc11 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -318,22 +318,36 @@ int APS5_VABI usleep_nid_postfix(KernelUseconds microseconds) { } #ifdef _WIN32 -static std::uint64_t CpuTimeNanos(bool callingThread, bool countKernel) { +static constexpr int GUEST_EIO = 5; +static constexpr int GUEST_EPERM = 1; + +int APS5_VABI ClockTimeWin32ErrorToGuestErrno_nid_no_patch(std::uint32_t error) { + if (error == ERROR_NOACCESS) return GUEST_EFAULT; + if (error == ERROR_ACCESS_DENIED) return GUEST_EPERM; + return GUEST_EIO; +} + +static bool CpuTimeNanos(bool callingThread, bool countKernel, std::uint64_t* nanos) { FILETIME creation{}; FILETIME exitTime{}; FILETIME kernel{}; FILETIME user{}; if (callingThread) { - if (!GetThreadTimes(GetCurrentThread(), &creation, &exitTime, &kernel, &user)) - throw std::system_error(GetLastError(), std::system_category(), "clock_gettime: GetThreadTimes failed"); + if (!GetThreadTimes(GetCurrentThread(), &creation, &exitTime, &kernel, &user)) { + *__error_nid_postfix() = ClockTimeWin32ErrorToGuestErrno_nid_no_patch(GetLastError()); + return false; + } } else { - if (!GetProcessTimes(GetCurrentProcess(), &creation, &exitTime, &kernel, &user)) - throw std::system_error(GetLastError(), std::system_category(), "clock_gettime: GetProcessTimes failed"); + if (!GetProcessTimes(GetCurrentProcess(), &creation, &exitTime, &kernel, &user)) { + *__error_nid_postfix() = ClockTimeWin32ErrorToGuestErrno_nid_no_patch(GetLastError()); + return false; + } } const auto toTicks = [](const FILETIME& time) { return (static_cast(time.dwHighDateTime) << 32) | time.dwLowDateTime; }; - return (toTicks(user) + (countKernel ? toTicks(kernel) : 0ULL)) * 100ULL; + *nanos = (toTicks(user) + (countKernel ? toTicks(kernel) : 0ULL)) * 100ULL; + return true; } static std::uint64_t CpuTimeResolutionNanos() { @@ -347,10 +361,6 @@ static std::uint64_t CpuTimeResolutionNanos() { return static_cast(increment) * 100ULL; } -static std::uint64_t ProcessCpuNanos(bool countSystem) { - return CpuTimeNanos(false, countSystem); -} - static std::uint64_t ProcessCpuResolutionNanos() { return CpuTimeResolutionNanos(); } @@ -390,7 +400,12 @@ static int ReadClock(int clockId, KernelTimespec* tp) { return 0; } if (clockId == 1 || clockId == 2) { +#ifdef _WIN32 + std::uint64_t nanos = 0; + if (!CpuTimeNanos(false, clockId == 2, &nanos)) return -1; +#else const std::uint64_t nanos = ProcessCpuNanos(clockId == 2); +#endif tp->tv_sec = static_cast(nanos / 1000000000ULL); tp->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; @@ -423,7 +438,8 @@ static int ReadClock(int clockId, KernelTimespec* tp) { return 0; } if (clockId == 14 || clockId == 15) { - const std::uint64_t nanos = CpuTimeNanos(clockId == 14, true); + std::uint64_t nanos = 0; + if (!CpuTimeNanos(clockId == 14, true, &nanos)) return -1; tp->tv_sec = static_cast(nanos / 1000000000ULL); tp->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; diff --git a/core/libs/prx/libkernel/Time/include/Time.hpp b/core/libs/prx/libkernel/Time/include/Time.hpp index dc1f2a0d85..925140891d 100644 --- a/core/libs/prx/libkernel/Time/include/Time.hpp +++ b/core/libs/prx/libkernel/Time/include/Time.hpp @@ -15,6 +15,9 @@ extern "C" { int APS5_VABI nanosleep_nid_postfix(const KernelTimespec* rqtp, KernelTimespec* rmtp); int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); +#ifdef _WIN32 + int APS5_VABI ClockTimeWin32ErrorToGuestErrno_nid_no_patch(std::uint32_t error); +#endif int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz); // Debug aid: APS5_TRACE_WAITS=1 aggregates blocking waits (event flags, semaphores, condition diff --git a/core/libs/tests/GuestCpuTimeClocks.cpp b/core/libs/tests/GuestCpuTimeClocks.cpp index 439a643457..cb7405736e 100644 --- a/core/libs/tests/GuestCpuTimeClocks.cpp +++ b/core/libs/tests/GuestCpuTimeClocks.cpp @@ -8,11 +8,20 @@ int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelClockGetres(KernelClockid clockId, KernelTimespec* tp); +#ifdef _WIN32 +int APS5_VABI ClockTimeWin32ErrorToGuestErrno_nid_no_patch(std::uint32_t error); +#endif int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); int APS5_VABI scePthreadJoin(Pthread thread, void** retval); } +#ifdef _WIN32 +static constexpr std::uint32_t WINDOWS_ERROR_NOACCESS = 998; +static constexpr std::uint32_t WINDOWS_ERROR_ACCESS_DENIED = 5; +static constexpr std::uint32_t WINDOWS_ERROR_INVALID_HANDLE = 6; +#endif + static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); @@ -98,6 +107,11 @@ static void ProcessClockCountsOtherThreads() { } int main() { +#ifdef _WIN32 + Require(ClockTimeWin32ErrorToGuestErrno_nid_no_patch(WINDOWS_ERROR_NOACCESS) == 14); + Require(ClockTimeWin32ErrorToGuestErrno_nid_no_patch(WINDOWS_ERROR_ACCESS_DENIED) == 1); + Require(ClockTimeWin32ErrorToGuestErrno_nid_no_patch(WINDOWS_ERROR_INVALID_HANDLE) == 5); +#endif KernelTimespec time{-1, -1}; Require(sceKernelClockGettime(GUEST_CLOCK_THREAD_CPUTIME_ID, &time) == SCE_OK); Require(time.tv_sec >= 0 && time.tv_nsec >= 0 && time.tv_nsec < NANOS_PER_SECOND); From e88f7fc1573557d785dc13f3b4f104133e77c2c2 Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 18 Oct 2026 17:14:22 +0000 Subject: [PATCH 131/131] fix(libkernel): return errno when Windows CPU clock queries fail --- core/libs/prx/libkernel/Time/Time.cpp | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 5169aebc11..4c447a1a45 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -350,19 +350,22 @@ static bool CpuTimeNanos(bool callingThread, bool countKernel, std::uint64_t* na return true; } -static std::uint64_t CpuTimeResolutionNanos() { +static bool CpuTimeResolutionNanos(std::uint64_t* nanos) { DWORD adjustment = 0; DWORD increment = 0; BOOL adjustmentDisabled = FALSE; - if (!GetSystemTimeAdjustment(&adjustment, &increment, &adjustmentDisabled)) - throw std::system_error(GetLastError(), std::system_category(), "clock_getres: GetSystemTimeAdjustment failed"); + if (!GetSystemTimeAdjustment(&adjustment, &increment, &adjustmentDisabled)) { + *__error_nid_postfix() = ClockTimeWin32ErrorToGuestErrno_nid_no_patch(GetLastError()); + return false; + } if (increment == 0) throw std::runtime_error("clock_getres: GetSystemTimeAdjustment reported no clock interrupt period"); - return static_cast(increment) * 100ULL; + *nanos = static_cast(increment) * 100ULL; + return true; } -static std::uint64_t ProcessCpuResolutionNanos() { - return CpuTimeResolutionNanos(); +static bool ProcessCpuResolutionNanos(std::uint64_t* nanos) { + return CpuTimeResolutionNanos(nanos); } #else static std::uint64_t ClockTicksPerSecond() { @@ -382,9 +385,10 @@ static std::uint64_t ProcessCpuNanos(bool countSystem) { return ticks / rate * 1000000000ULL + ticks % rate * 1000000000ULL / rate; } -static std::uint64_t ProcessCpuResolutionNanos() { +static bool ProcessCpuResolutionNanos(std::uint64_t* nanos) { const std::uint64_t rate = ClockTicksPerSecond(); - return (1000000000ULL + rate - 1ULL) / rate; + *nanos = (1000000000ULL + rate - 1ULL) / rate; + return true; } #endif @@ -530,7 +534,8 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { if (IsCpuClock(clockId)) return clock_getres_nid_postfix(GUEST_CLOCK_THREAD_CPUTIME_ID, res); if (clockId == 1 || clockId == 2) { - const std::uint64_t nanos = ProcessCpuResolutionNanos(); + std::uint64_t nanos = 0; + if (!ProcessCpuResolutionNanos(&nanos)) return -1; res->tv_sec = static_cast(nanos / 1000000000ULL); res->tv_nsec = static_cast(nanos % 1000000000ULL); return 0; @@ -558,7 +563,8 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res) { return 0; } if (clockId == 14 || clockId == 15) { - const std::uint64_t nanos = CpuTimeResolutionNanos(); + std::uint64_t nanos = 0; + if (!CpuTimeResolutionNanos(&nanos)) return -1; res->tv_sec = static_cast(nanos / 1000000000ULL); res->tv_nsec = static_cast(nanos % 1000000000ULL); return 0;